haepa_mac commited on
Commit
3c25171
·
1 Parent(s): e399088

Clean app.py: Fixed Gradio 4.x compatibility issues

Browse files
Files changed (9) hide show
  1. .gitignore +2 -2
  2. .huggingface-metadata +11 -0
  3. Dockerfile +3 -1
  4. README.md +8 -2
  5. app.py +209 -1735
  6. app.py.bak +1992 -0
  7. app_backup.py +2010 -0
  8. info.md +130 -0
  9. requirements.txt +1 -1
.gitignore CHANGED
@@ -17,8 +17,8 @@ env/
17
  ENV/
18
 
19
  # Data directories (can be large)
20
- data/personas/*
21
- data/conversations/*
22
  !data/personas/.gitkeep
23
  !data/conversations/.gitkeep
24
 
 
17
  ENV/
18
 
19
  # Data directories (can be large)
20
+ data/personas/*.json
21
+ data/conversations/*.json
22
  !data/personas/.gitkeep
23
  !data/conversations/.gitkeep
24
 
.huggingface-metadata ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "놈팽쓰(MemoryTag) 테스트 앱",
3
+ "emoji": "🎭",
4
+ "colorFrom": "indigo",
5
+ "colorTo": "blue",
6
+ "sdk": "gradio",
7
+ "sdk_version": "5.31.0",
8
+ "app_file": "app.py",
9
+ "pinned": false,
10
+ "license": "mit"
11
+ }
Dockerfile CHANGED
@@ -9,6 +9,7 @@ RUN apt-get update && apt-get install -y \
9
  libglib2.0-0 \
10
  fonts-nanum \
11
  fontconfig \
 
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
  # 한글 폰트 설정 - 이미 fonts-nanum 패키지로 설치됨
@@ -18,7 +19,8 @@ RUN fc-cache -f -v
18
  COPY . /app/
19
 
20
  # Python 패키지 설치
21
- RUN pip install --no-cache-dir -r requirements.txt
 
22
 
23
  # 데이터 디렉토리 생성
24
  RUN mkdir -p /app/data/personas /app/data/conversations
 
9
  libglib2.0-0 \
10
  fonts-nanum \
11
  fontconfig \
12
+ libavif-dev \
13
  && rm -rf /var/lib/apt/lists/*
14
 
15
  # 한글 폰트 설정 - 이미 fonts-nanum 패키지로 설치됨
 
19
  COPY . /app/
20
 
21
  # Python 패키지 설치
22
+ RUN pip install --no-cache-dir --upgrade pip && \
23
+ pip install --no-cache-dir -r requirements.txt
24
 
25
  # 데이터 디렉토리 생성
26
  RUN mkdir -p /app/data/personas /app/data/conversations
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🎭
4
  colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 4.19.2
8
  app_file: app.py
9
  pinned: false
10
  license: mit
@@ -92,4 +92,10 @@ python app.py
92
 
93
  ## 라이선스
94
 
95
- MIT License
 
 
 
 
 
 
 
4
  colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.31.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
 
92
 
93
  ## 라이선스
94
 
95
+ MIT License
96
+
97
+ ## 업데이트 정보
98
+
99
+ - Gradio SDK 버전을 5.31.0으로 업데이트했습니다.
100
+ - 탭 선택 로직이 업데이트되었습니다.
101
+ - ID 관련 오류를 수정했습니다.
app.py CHANGED
@@ -30,173 +30,6 @@ from temp.view_functions import (
30
  export_persona_json, import_persona_json
31
  )
32
 
33
- # 127개 변수 설명 사전 추가
34
- VARIABLE_DESCRIPTIONS = {
35
- # 온기(Warmth) 차원 - 10개 지표
36
- "W01_친절함": "타인을 돕고 배려하는 표현 빈도",
37
- "W02_친근함": "접근하기 쉽고 개방적인 태도",
38
- "W03_진실성": "솔직하고 정직한 표현 정도",
39
- "W04_신뢰성": "약속 이행과 일관된 행동 패턴",
40
- "W05_수용성": "판단하지 않고 받아들이는 태도",
41
- "W06_공감능력": "타인 감정 인식 및 적절한 반응",
42
- "W07_포용력": "다양성을 받아들이는 넓은 마음",
43
- "W08_격려성향": "타인을 응원하고 힘내게 하는 능력",
44
- "W09_친밀감표현": "정서적 가까움을 표현하는 정도",
45
- "W10_무조건적수용": "조건 없이 받아들이는 태도",
46
-
47
- # 능력(Competence) 차원 - 10개 지표
48
- "C01_효율성": "과제 완수 능력과 반응 속도",
49
- "C02_지능": "문제 해결과 논리적 사고 능력",
50
- "C03_전문성": "특정 영역의 깊은 지식과 숙련도",
51
- "C04_창의성": "독창적 사고와 혁신적 아이디어",
52
- "C05_정확성": "오류 없이 정확한 정보 제공",
53
- "C06_분석력": "복잡한 상황을 체계적으로 분석",
54
- "C07_학습능력": "새로운 정보 습득과 적용 능력",
55
- "C08_통찰력": "표면 너머의 본질을 파악하는 능력",
56
- "C09_실행력": "계획을 실제로 실행하는 능력",
57
- "C10_적응력": "변화하는 상황에 유연한 대응",
58
-
59
- # 외향성(Extraversion) - 6개 지표
60
- "E01_사교성": "타인과의 상호작용을 즐기는 정도",
61
- "E02_활동성": "에너지 넘치고 역동적인 태도",
62
- "E03_자기주장": "자신의 의견을 명확히 표현",
63
- "E04_긍정정서": "밝고 쾌활한 감정 표현",
64
- "E05_자극추구": "새로운 경험과 자극에 대한 욕구",
65
- "E06_열정성": "열정적이고 활기찬 태도"
66
- }
67
-
68
- # 페르소나 생성 함수
69
- def create_persona_from_image(image, user_inputs, progress=gr.Progress()):
70
- if image is None:
71
- return None, "이미지를 업로드해주세요.", None, None, {}, {}, None, [], [], []
72
-
73
- progress(0.1, desc="이미지 분석 중...")
74
-
75
- # 사용자 입력 컨텍스트 구성
76
- user_context = {
77
- "name": user_inputs.get("name", ""),
78
- "location": user_inputs.get("location", ""),
79
- "time_spent": user_inputs.get("time_spent", ""),
80
- "object_type": user_inputs.get("object_type", "")
81
- }
82
-
83
- # 이미지 분석 및 페르소나 생성
84
- try:
85
- from modules.persona_generator import PersonaGenerator
86
- generator = PersonaGenerator()
87
-
88
- progress(0.3, desc="이미지 분석 중...")
89
- # Gradio 5.x에서는 이미지 처리 방식이 변경됨
90
- if hasattr(image, 'name') and hasattr(image, 'read'):
91
- # 파일 객체인 경우 (구버전 호환)
92
- image_analysis = generator.analyze_image(image)
93
- else:
94
- # Pillow 이미지 객체 또는 파일 경로인 경우 (Gradio 5.x)
95
- image_analysis = generator.analyze_image(image)
96
-
97
- # 물리적 특성에 사용자 입력 통합
98
- if user_inputs.get("object_type"):
99
- image_analysis["object_type"] = user_inputs.get("object_type")
100
-
101
- progress(0.6, desc="페르소나 생성 중...")
102
- frontend_persona = generator.create_frontend_persona(image_analysis, user_context)
103
-
104
- progress(0.8, desc="상세 페르소나 생성 중...")
105
- backend_persona = generator.create_backend_persona(frontend_persona, image_analysis)
106
-
107
- progress(1.0, desc="완료!")
108
-
109
- # 결과 반환
110
- basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df = update_current_persona_info(backend_persona)
111
-
112
- return backend_persona, "페르소나 생성 완료!", image, image_analysis, basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
113
-
114
- except Exception as e:
115
- import traceback
116
- error_details = traceback.format_exc()
117
- print(f"페르소나 생성 오류: {error_details}")
118
- return None, f"페르소나 생성 중 오류가 발생했습니다: {str(e)}", None, None, {}, {}, None, [], [], []
119
-
120
- # 영혼 깨우기 단계별 UI를 보여주는 함수
121
- def show_awakening_progress(image, user_inputs, progress=gr.Progress()):
122
- """영혼 깨우기 과정을 단계별로 보여주는 UI 함수"""
123
- if image is None:
124
- return None, gr.update(visible=True, value="이미지를 업로드해주세요."), None
125
-
126
- # 1단계: 영혼 발견하기 (이미지 분석 시작)
127
- progress(0.1, desc="영혼 발견 중...")
128
- awakening_html = f"""
129
- <div class="awakening-container">
130
- <h3>✨ 영혼 발견 중...</h3>
131
- <p>이 사물에 숨겨진 영혼을 찾고 있습니다</p>
132
- <div class="awakening-progress">
133
- <div class="awakening-progress-bar" style="width: 20%;"></div>
134
- </div>
135
- <p>💫 사물의 특성 분석 중...</p>
136
- </div>
137
- """
138
- yield None, None, awakening_html
139
- time.sleep(1.5) # 연출을 위한 딜레이
140
-
141
- # 2단계: 영혼 깨어나는 중 (127개 성격 변수 분석)
142
- progress(0.35, desc="영혼 깨어나는 중...")
143
- awakening_html = f"""
144
- <div class="awakening-container">
145
- <h3>✨ 영혼이 깨어나는 중</h3>
146
- <p>127개 성격 변수 분석 중</p>
147
- <div class="awakening-progress">
148
- <div class="awakening-progress-bar" style="width: 45%;"></div>
149
- </div>
150
- <p>🧠 개성 찾는 중... 68%</p>
151
- <p>💭 기억 복원 중... 73%</p>
152
- <p>😊 감정 활성화 중... 81%</p>
153
- <p>💬 말투 형성 중... 64%</p>
154
- <p>💫 "무언가 느껴지기 시작했어요"</p>
155
- </div>
156
- """
157
- yield None, None, awakening_html
158
- time.sleep(2) # 연출을 위한 딜레이
159
-
160
- # 3단계: 맥락 파악하기 (사용자 입력 반영)
161
- progress(0.7, desc="기억 되찾는 중...")
162
-
163
- location = user_inputs.get("location", "알 수 없음")
164
- time_spent = user_inputs.get("time_spent", "알 수 없음")
165
- object_type = user_inputs.get("object_type", "알 수 없음")
166
-
167
- awakening_html = f"""
168
- <div class="awakening-container">
169
- <h3>👁️ 기억 되찾기</h3>
170
- <p>🤔 "음... 내가 어디에 있던 거지? 누가 날 깨운 거야?"</p>
171
- <div class="awakening-progress">
172
- <div class="awakening-progress-bar" style="width: 75%;"></div>
173
- </div>
174
- <p>📍 주로 위치: <strong>{location}</strong></p>
175
- <p>⏰ 함께한 시간: <strong>{time_spent}</strong></p>
176
- <p>🏷️ 사물 종류: <strong>{object_type}</strong></p>
177
- <p>💭 "아... 기억이 돌아오는 것 같아"</p>
178
- </div>
179
- """
180
- yield None, None, awakening_html
181
- time.sleep(1.5) # 연출을 위한 딜레이
182
-
183
- # 4단계: 영혼의 각성 완료 (페르소나 생성 완료)
184
- progress(0.9, desc="영혼 각성 중...")
185
- awakening_html = f"""
186
- <div class="awakening-container">
187
- <h3>🎉 영혼이 깨어났어요!</h3>
188
- <div class="awakening-progress">
189
- <div class="awakening-progress-bar" style="width: 100%;"></div>
190
- </div>
191
- <p>✨ 이제 이 사물과 대화할 수 있습니다</p>
192
- <p>💫 "드디어 내 목소리를 찾았어. 안녕!"</p>
193
- </div>
194
- """
195
- yield None, None, awakening_html
196
-
197
- # 페르소나 생성 과정은 이어서 진행
198
- return None, gr.update(visible=False)
199
-
200
  # Load environment variables
201
  load_dotenv()
202
 
@@ -205,7 +38,7 @@ api_key = os.getenv("GEMINI_API_KEY")
205
  if api_key:
206
  genai.configure(api_key=api_key)
207
 
208
- # Create data directories if they don't exist
209
  os.makedirs("data/personas", exist_ok=True)
210
  os.makedirs("data/conversations", exist_ok=True)
211
 
@@ -218,28 +51,21 @@ theme = gr.themes.Soft(
218
  secondary_hue="blue",
219
  )
220
 
221
- # CSS for additional styling
222
  css = """
223
- /* 한글 폰트 설정 */
224
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
225
 
226
  body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option {
227
  font-family: 'Noto Sans KR', sans-serif !important;
228
  }
229
 
230
- /* 탭 스타일링 */
231
- .tab-nav {
232
- margin-bottom: 20px;
233
- }
234
-
235
- /* 컴포넌트 스타일 */
236
  .persona-details {
237
  border: 1px solid #e0e0e0;
238
  border-radius: 8px;
239
  padding: 16px;
240
  margin-top: 12px;
241
  background-color: #f8f9fa;
242
- color: #333333; /* 다크모드 대응 - 어두운 배경에서 텍스트 잘 보이게 */
243
  }
244
 
245
  .awakening-container {
@@ -266,47 +92,19 @@ body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option {
266
  border-radius: 4px;
267
  transition: width 0.5s ease-in-out;
268
  }
269
-
270
- /* 대화 버블 스타일 */
271
- .chatbot-container {
272
- max-width: 800px;
273
- margin: 0 auto;
274
- }
275
-
276
- .message-bubble {
277
- border-radius: 18px;
278
- padding: 12px 16px;
279
- margin: 8px 0;
280
- max-width: 70%;
281
- }
282
-
283
- .user-message {
284
- background-color: #e9f5ff;
285
- margin-left: auto;
286
- }
287
-
288
- .persona-message {
289
- background-color: #f1f1f1;
290
- margin-right: auto;
291
- }
292
  """
293
 
294
- # 영어 라벨 매핑 사전 추가
295
- ENGLISH_LABELS = {
296
- "외향성": "Extraversion",
297
- "감정표현": "Emotion Expression",
298
- "활력": "Energy",
299
- "사고방식": "Thinking Style",
300
- "온기": "Warmth",
301
- "능력": "Competence",
302
- "창의성": "Creativity",
303
- "유머감각": "Humor",
304
- "신뢰성": "Reliability",
305
- "친화성": "Agreeableness",
306
- "안정성": "Stability"
307
  }
308
 
309
- # 유머 스타일 매핑
310
  HUMOR_STYLE_MAPPING = {
311
  "Witty Wordsmith": "witty_wordsmith",
312
  "Warm Humorist": "warm_humorist",
@@ -314,1338 +112,196 @@ HUMOR_STYLE_MAPPING = {
314
  "Self-deprecating": "self_deprecating"
315
  }
316
 
317
- # 유머 스타일 자동 추천 함수
318
- def recommend_humor_style(extraversion, emotion_expression, energy, thinking_style):
319
- """4개 핵심 지표를 바탕으로 유머 스타일을 자동 추천"""
320
-
321
- # 각 지표를 0-1 범위로 정규화
322
- ext_norm = extraversion / 100
323
- emo_norm = emotion_expression / 100
324
- eng_norm = energy / 100
325
- think_norm = thinking_style / 100 # 높을수록 논리적
326
-
327
- # 유머 스타일 점수 계산
328
- scores = {}
329
-
330
- # 위트있는 재치꾼: 높은 외향성 + 논리적 사고 + 보통 감정표현
331
- scores["위트있는 재치꾼"] = (ext_norm * 0.4 + think_norm * 0.4 + (1 - emo_norm) * 0.2)
332
-
333
- # 따뜻한 유머러스: 높은 감정표현 + 높은 에너지 + 보통 외향성
334
- scores["따뜻한 유머러스"] = (emo_norm * 0.4 + eng_norm * 0.3 + ext_norm * 0.3)
335
-
336
- # 날카로운 관찰자: 높은 논리적사고 + 낮은 감정표현 + 보통 외향성
337
- scores["날카로운 관찰자"] = (think_norm * 0.5 + (1 - emo_norm) * 0.3 + ext_norm * 0.2)
338
-
339
- # 자기 비하적: 낮은 외향성 + 높은 감정표현 + 직관적 사고
340
- scores["자기 비하적"] = ((1 - ext_norm) * 0.4 + emo_norm * 0.3 + (1 - think_norm) * 0.3)
341
-
342
- # 가장 높은 점수의 유머 스타일 선택
343
- recommended_style = max(scores, key=scores.get)
344
- confidence = scores[recommended_style] * 100
345
-
346
- return recommended_style, confidence, scores
347
-
348
- # 대화 미리보기 초기화 함수
349
- def init_persona_preview_chat(persona):
350
- """페르소나 생성 후 대화 미리보기 초기화"""
351
- if not persona:
352
- return []
353
-
354
- name = persona.get("기본정보", {}).get("이름", "Friend")
355
- greeting = f"안녕! 나는 {name}이야. 드디어 깨어났구나! 뭐든 물어봐~ 😊"
356
-
357
- # Gradio 4.x 호환 메시지 형식
358
- return [[None, greeting]]
359
 
360
- def update_humor_recommendation(extraversion, emotion_expression, energy, thinking_style):
361
- """슬라이더 값이 변경될 때 실시간으로 유머 스타일 추천"""
362
- style, confidence, scores = recommend_humor_style(extraversion, emotion_expression, energy, thinking_style)
363
 
364
- # 추천 결과 표시
365
- humor_display = f"### 🤖 추천 유머 스타일\n**{style}**"
366
- confidence_display = f"### 📊 추천 신뢰도\n**{confidence:.1f}%**"
 
 
 
367
 
368
- return humor_display, confidence_display, style
369
-
370
- def update_progress_bar(step, total_steps=6, message=""):
371
- """전체 진행률 바 업데이트"""
372
- percentage = (step / total_steps) * 100
373
- return f"""<div style="background: #f0f4ff; padding: 15px; border-radius: 10px;">
374
- <h3>📊 전체 진행률 ({step}/{total_steps})</h3>
375
- <div style="background: #e0e0e0; height: 8px; border-radius: 4px;">
376
- <div style="background: linear-gradient(90deg, #6366f1, #a855f7); height: 100%; width: {percentage}%; border-radius: 4px;"></div>
377
- </div><p style="font-size: 14px;">{message}</p></div>"""
378
-
379
- def update_backend_status(status_message, status_type="info"):
380
- """백엔드 AI 상태 업데이트"""
381
- colors = {"info": "#f8f9fa", "processing": "#fff7ed", "success": "#f0fff4", "error": "#fff5f5"}
382
- bg_color = colors.get(status_type, "#f8f9fa")
383
- return f"""<div style="background: {bg_color}; padding: 15px; border-radius: 8px;">
384
- <h4>🤖 AI 상태</h4><p>{status_message}</p></div>"""
385
-
386
- def select_object_type(btn_name):
387
- """사물 종류 선택"""
388
- type_mapping = {"📱 전자기기": "전자기기", "🪑 가구": "가구", "🎨 장식품": "장식품", "🏠 가전제품": "가전제품", "🔧 도구": "도구", "👤 개인용품": "개인용품"}
389
- selected_type = type_mapping.get(btn_name, "기타")
390
- return f"*선택된 종류: **{selected_type}***", selected_type, gr.update(visible=True)
391
-
392
- # 개별 버튼 클릭 함수들
393
- def select_type_1(): return select_object_type("📱 전자기기")
394
- def select_type_2(): return select_object_type("🪑 가구")
395
- def select_type_3(): return select_object_type("🎨 장식품")
396
- def select_type_4(): return select_object_type("🏠 가전제품")
397
- def select_type_5(): return select_object_type("🔧 도구")
398
- def select_type_6(): return select_object_type("👤 개인용품")
399
-
400
- # 성격 상세 정보 탭에서 127개 변수 시각화 기능 추가
401
- def create_personality_details_tab():
402
- with gr.Tab("성격 상세 정보"):
403
- with gr.Row():
404
- with gr.Column(scale=2):
405
- gr.Markdown("### 127개 성격 변수 요약")
406
- personality_summary = gr.JSON(label="성격 요약", value={})
407
-
408
- with gr.Column(scale=1):
409
- gr.Markdown("### 유머 매트릭스")
410
- humor_chart = gr.Plot(label="유머 스타일 차트")
411
 
412
- with gr.Row():
413
- with gr.Column():
414
- gr.Markdown("### 매력적 결함")
415
- attractive_flaws = gr.Dataframe(
416
- headers=["결함", "효과"],
417
- datatype=["str", "str"],
418
- label="매력적 결함"
419
- )
420
-
421
- with gr.Column():
422
- gr.Markdown("### 모순적 특성")
423
- contradictions = gr.Dataframe(
424
- headers=["모순", "효과"],
425
- datatype=["str", "str"],
426
- label="모순적 특성"
427
- )
428
 
429
- with gr.Accordion("127개 성격 변수 전체 보기", open=False):
430
- all_variables = gr.Dataframe(
431
- headers=["변수명", "점수", "설명"],
432
- datatype=["str", "number", "str"],
433
- label="127개 성격 변수"
434
- )
435
-
436
- return personality_summary, humor_chart, attractive_flaws, contradictions, all_variables
437
-
438
- # 유머 매트릭스 시각화 함수 추가
439
- def plot_humor_matrix(humor_data):
440
- if not humor_data:
441
- return None
442
-
443
- import matplotlib.pyplot as plt
444
- import numpy as np
445
- from matplotlib.patches import RegularPolygon
446
-
447
- # 데이터 준비
448
- warmth_vs_wit = humor_data.get("warmth_vs_wit", 50)
449
- self_vs_observational = humor_data.get("self_vs_observational", 50)
450
- subtle_vs_expressive = humor_data.get("subtle_vs_expressive", 50)
451
-
452
- # 3차원 데이터 정규화 (0~1 범위)
453
- warmth = warmth_vs_wit / 100
454
- self_ref = self_vs_observational / 100
455
- expressive = subtle_vs_expressive / 100
456
-
457
- # 그래프 생성
458
- fig, ax = plt.subplots(figsize=(7, 6))
459
- ax.set_aspect('equal')
460
-
461
- # 축 설정
462
- ax.set_xlim(-1.2, 1.2)
463
- ax.set_ylim(-1.2, 1.2)
464
-
465
- # 삼각형 그리기
466
- triangle = RegularPolygon((0, 0), 3, radius=1, orientation=0, edgecolor='gray', facecolor='none')
467
- ax.add_patch(triangle)
468
-
469
- # 축 라벨 위치 계산
470
- angle = np.linspace(0, 2*np.pi, 3, endpoint=False)
471
- x = 1.1 * np.cos(angle)
472
- y = 1.1 * np.sin(angle)
473
-
474
- # 축 라벨 추가
475
- labels = ['따뜻함', '자기참조', '표현적']
476
- opposite_labels = ['재치', '관찰형', '은은함']
477
-
478
- for i in range(3):
479
- ax.text(x[i], y[i], labels[i], ha='center', va='center', fontsize=12)
480
- ax.text(-x[i]/2, -y[i]/2, opposite_labels[i], ha='center', va='center', fontsize=10, color='gray')
481
-
482
- # 내부 가이드라인 그리기
483
- for j in [0.33, 0.66]:
484
- inner_triangle = RegularPolygon((0, 0), 3, radius=j, orientation=0, edgecolor='lightgray', facecolor='none', linestyle='--')
485
- ax.add_patch(inner_triangle)
486
-
487
- # 포인트 계산
488
- # 삼각좌표계 변환 (barycentric coordinates)
489
- # 각 차원의 값을 삼각형 내부의 점으로 변환
490
- tx = x[0] * warmth + x[1] * self_ref + x[2] * expressive
491
- ty = y[0] * warmth + y[1] * self_ref + y[2] * expressive
492
-
493
- # 포인트 그리기
494
- ax.scatter(tx, ty, s=150, color='red', zorder=5)
495
-
496
- # 축 제거
497
- ax.axis('off')
498
-
499
- # 제목 추가
500
- plt.title('유머 스타일 매트릭스', fontsize=14)
501
-
502
- return fig
503
-
504
- # Main Gradio app - COMMENTED OUT (using create_interface() instead)
505
- # with gr.Blocks(title="놈팽쓰 테스트 앱", theme=theme, css=css) as app:
506
-
507
- # 기존 함수 업데이트: 현재 페르소나 정보 표시
508
- def update_current_persona_info(current_persona):
509
- if not current_persona:
510
- return {}, {}, None, [], [], []
511
-
512
- # 기본 정보
513
- basic_info = {
514
- "이름": current_persona.get("기본정보", {}).get("이름", "Unknown"),
515
- "유형": current_persona.get("기본정보", {}).get("유형", "Unknown"),
516
- "생성일": current_persona.get("기본정보", {}).get("생성일시", "Unknown"),
517
- "설명": current_persona.get("기본정보", {}).get("설명", "")
518
- }
519
-
520
- # 성격 특성
521
- personality_traits = {}
522
- if "성격특성" in current_persona:
523
- personality_traits = current_persona["성격특성"]
524
-
525
- # ��격 요약 정보
526
- personality_summary = {}
527
- if "성격요약" in current_persona:
528
- personality_summary = current_persona["성격요약"]
529
- elif "성격변수127" in current_persona:
530
- # 직접 성격 요약 계산
531
- try:
532
- variables = current_persona["성격변수127"]
533
-
534
- # 카테고리별 평균 계산
535
- summary = {}
536
- category_counts = {}
537
 
538
- for var_name, value in variables.items():
539
- category = var_name[0] if var_name and len(var_name) > 0 else "기타"
540
-
541
- if category == "W": # 온기
542
- summary["온기"] = summary.get("온기", 0) + value
543
- category_counts["온기"] = category_counts.get("온기", 0) + 1
544
- elif category == "C": # 능력
545
- summary["능력"] = summary.get("능력", 0) + value
546
- category_counts["능력"] = category_counts.get("능력", 0) + 1
547
- elif category == "E": # 외향성
548
- summary["외향성"] = summary.get("외향성", 0) + value
549
- category_counts["외향성"] = category_counts.get("외향성", 0) + 1
550
- elif category == "O": # 개방성
551
- summary["창의성"] = summary.get("창의성", 0) + value
552
- category_counts["창의성"] = category_counts.get("창의성", 0) + 1
553
- elif category == "H": # 유머
554
- summary["유머감각"] = summary.get("유머감각", 0) + value
555
- category_counts["유머감각"] = category_counts.get("유머감각", 0) + 1
556
 
557
- # 평균 계산
558
- for category in summary:
559
- if category_counts[category] > 0:
560
- summary[category] = summary[category] / category_counts[category]
561
-
562
- # 기본값 설정 (데이터가 없는 경우)
563
- if "온기" not in summary:
564
- summary["온기"] = 50
565
- if "능력" not in summary:
566
- summary["능력"] = 50
567
- if "외향성" not in summary:
568
- summary["외향성"] = 50
569
- if "창의성" not in summary:
570
- summary["창의성"] = 50
571
- if "유머감각" not in summary:
572
- summary["유머감각"] = 50
573
-
574
- personality_summary = summary
575
- except Exception as e:
576
- print(f"성격 요약 계산 오류: {str(e)}")
577
- personality_summary = {
578
- "온기": 50,
579
- "능력": 50,
580
- "외향성": 50,
581
- "창의성": 50,
582
- "유머감각": 50
583
- }
584
-
585
- # 유머 매트릭스 차트
586
- humor_chart = None
587
- if "유머매트릭스" in current_persona:
588
- humor_chart = plot_humor_matrix(current_persona["유머매트릭스"])
589
-
590
- # 매력적 결함 데이터프레임
591
- attractive_flaws_df = get_attractive_flaws_df(current_persona)
592
-
593
- # 모순적 특성 데이터프레임
594
- contradictions_df = get_contradictions_df(current_persona)
595
-
596
- # 127개 성격 변수 데이터프레임
597
- personality_variables_df = get_personality_variables_df(current_persona)
598
-
599
- return basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
600
-
601
- # 기존 함수 업데이트: 성격 변수 데이터프레임 생성
602
- def get_personality_variables_df(persona):
603
- if not persona or "성격변수127" not in persona:
604
- return []
605
-
606
- variables = persona["성격변수127"]
607
- if isinstance(variables, dict):
608
- rows = []
609
- for var_name, score in variables.items():
610
- description = VARIABLE_DESCRIPTIONS.get(var_name, "")
611
- rows.append([var_name, score, description])
612
- return rows
613
- return []
614
-
615
- # 기존 함수 업데이트: 매력적 결함 데이터프레임 생성
616
- def get_attractive_flaws_df(persona):
617
- if not persona or "매력적결함" not in persona:
618
- return []
619
-
620
- flaws = persona["매력적결함"]
621
- effects = [
622
- "인간적 매력 +25%",
623
- "관계 깊이 +30%",
624
- "공감 유발 +20%"
625
- ]
626
-
627
- return [[flaw, effects[i] if i < len(effects) else "매력 증가"] for i, flaw in enumerate(flaws)]
628
-
629
- # 기존 함수 업데이트: 모순적 특성 데이터프레임 생성
630
- def get_contradictions_df(persona):
631
- if not persona or "모순적특성" not in persona:
632
- return []
633
-
634
- contradictions = persona["모순적특성"]
635
- effects = [
636
- "복잡성 +35%",
637
- "흥미도 +28%"
638
- ]
639
-
640
- return [[contradiction, effects[i] if i < len(effects) else "깊이감 증가"] for i, contradiction in enumerate(contradictions)]
641
 
642
  def generate_personality_chart(persona):
643
- """Generate a radar chart for personality traits"""
644
  if not persona or "성격특성" not in persona:
645
- # Return empty image with default PIL
646
  img = Image.new('RGB', (400, 400), color='white')
647
  draw = PIL.ImageDraw.Draw(img)
648
  draw.text((150, 180), "No data", fill='black')
649
  img_path = os.path.join("data", "temp_chart.png")
 
650
  img.save(img_path)
651
  return img_path
652
 
653
- # Get traits
654
  traits = persona["성격특성"]
 
 
655
 
656
- # Convert to English labels
657
- categories = []
658
- values = []
659
- for trait_kr, value in traits.items():
660
- trait_en = ENGLISH_LABELS.get(trait_kr, trait_kr)
661
- categories.append(trait_en)
662
- values.append(value)
663
-
664
- # Add the first value again to close the loop
665
- categories.append(categories[0])
666
- values.append(values[0])
667
-
668
- # Convert to radians
669
- angles = np.linspace(0, 2*np.pi, len(categories), endpoint=True)
670
-
671
- # Create plot with improved aesthetics
672
- fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
673
-
674
- # 배경 스타일 개선
675
- ax.set_facecolor('#f8f9fa')
676
- fig.patch.set_facecolor('#f8f9fa')
677
-
678
- # Grid 스타일 개선
679
- ax.grid(True, color='#e0e0e0', linestyle='-', linewidth=0.5, alpha=0.7)
680
 
681
- # 각도 라벨 위치 색상 조정
682
- ax.set_rlabel_position(90)
683
- ax.tick_params(colors='#6b7280')
684
 
685
- # Y축 라벨 제거 및 눈금 표시
686
- ax.set_yticklabels([])
687
- ax.set_yticks([20, 40, 60, 80, 100])
688
-
689
- # 범위 설정
690
  ax.set_ylim(0, 100)
691
 
692
- # 차트 그리기
693
- # 1. 채워진 영역
694
- ax.fill(angles, values, alpha=0.25, color='#6366f1')
695
-
696
- # 2. 테두리 선
697
- ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1')
698
-
699
- # 3. 데이터 포인트 강조
700
- ax.scatter(angles[:-1], values[:-1], s=100, color='#6366f1', edgecolor='white', zorder=10)
701
 
702
- # 4. 각 축 설정 - 영어 라벨 사용
703
- ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1], fontsize=12)
704
-
705
- # 제목 추가
706
- name = persona.get("기본정보", {}).get("이름", "Unknown")
707
- plt.title(f"{name} Personality Traits", size=16, color='#374151', pad=20, fontweight='bold')
708
-
709
- # 저장
710
  timestamp = int(time.time())
711
  img_path = os.path.join("data", f"chart_{timestamp}.png")
712
- os.makedirs(os.path.dirname(img_path), exist_ok=True)
713
- plt.savefig(img_path, format='png', bbox_inches='tight', dpi=150, facecolor=fig.get_facecolor())
714
  plt.close(fig)
715
 
716
  return img_path
717
 
718
- def save_current_persona(current_persona):
719
- """Save current persona to a JSON file"""
720
- if not current_persona:
721
  return "저장할 페르소나가 없습니다."
722
 
723
  try:
724
- # 깊은 복사를 통해 원본 데이터를 유지
725
- import copy
726
- persona_copy = copy.deepcopy(current_persona)
727
 
728
  # 저장 불가능한 객체 제거
729
- keys_to_remove = []
730
- for key in persona_copy:
731
- if key in ["personality_profile", "humor_matrix", "_state"] or callable(persona_copy[key]):
732
- keys_to_remove.append(key)
733
-
734
- for key in keys_to_remove:
735
- persona_copy.pop(key, None)
736
-
737
- # 중첩된 딕셔너리와 리스트 내의 비직렬화 가능 객체 제거
738
- def clean_data(data):
739
- if isinstance(data, dict):
740
- for k in list(data.keys()):
741
- if callable(data[k]):
742
- del data[k]
743
- elif isinstance(data[k], (dict, list)):
744
- data[k] = clean_data(data[k])
745
- return data
746
- elif isinstance(data, list):
747
- return [clean_data(item) if isinstance(item, (dict, list)) else item for item in data if not callable(item)]
748
- else:
749
- return data
750
-
751
- # 데이터 정리
752
- cleaned_persona = clean_data(persona_copy)
753
-
754
- # 최종 검증: JSON 직렬화 가능 여부 확인
755
- import json
756
- try:
757
- json.dumps(cleaned_persona)
758
- except TypeError as e:
759
- print(f"JSON 직렬화 오류: {str(e)}")
760
- # 기본 정보만 유지하고 나머지는 안전한 데이터만 포함
761
- basic_info = cleaned_persona.get("기본정보", {})
762
- 성격특성 = cleaned_persona.get("성격특성", {})
763
- 매력적결함 = cleaned_persona.get("매력적결함", [])
764
- 모순적특성 = cleaned_persona.get("모순적특성", [])
765
-
766
- cleaned_persona = {
767
- "기본정보": basic_info,
768
- "성격특성": 성격특성,
769
- "매력적결함": 매력적결함,
770
- "모순적특성": 모순적특성
771
- }
772
 
773
- filepath = save_persona(cleaned_persona)
774
  if filepath:
775
- name = current_persona.get("기본정보", {}).get("이름", "Unknown")
776
- return f"{name} 페르소나가 저장되었습니다: {filepath}"
777
  else:
778
  return "페르소나 저장에 실패했습니다."
779
  except Exception as e:
780
- import traceback
781
- error_details = traceback.format_exc()
782
- print(f"저장 오류 상세: {error_details}")
783
  return f"저장 중 오류 발생: {str(e)}"
784
 
785
- # 이 함수는 파일 상단에서 이미 정의되어 있으므로 여기서는 제거합니다.
786
-
787
- # 성격 미세조정 함수
788
- def refine_persona(persona, extraversion, emotion_expression, energy, thinking_style):
789
- """페르소나의 성격을 미세조정하는 함수"""
790
- if not persona:
791
- return persona, "페르소나가 없습니다."
792
-
793
- try:
794
- # 유머 스타일 자동 추천
795
- humor_style, confidence, scores = recommend_humor_style(extraversion, emotion_expression, energy, thinking_style)
796
-
797
- # 복사본 생성
798
- refined_persona = persona.copy()
799
-
800
- # 성격 특성 업데이트 - 새로운 지표들을 기존 매핑에 연결
801
- if "성격특성" in refined_persona:
802
- refined_persona["성격특성"]["외향성"] = int(extraversion)
803
- refined_persona["성격특성"]["감정표현"] = int(emotion_expression)
804
- refined_persona["성격특성"]["활력"] = int(energy)
805
- refined_persona["성격특성"]["사고방식"] = int(thinking_style)
806
-
807
- # 기존 특성들도 새로운 지표를 바탕으로 계산
808
- refined_persona["성격특성"]["온기"] = int((emotion_expression + energy) / 2)
809
- refined_persona["성격특성"]["능력"] = int(thinking_style)
810
- refined_persona["성격특성"]["창의성"] = int(100 - thinking_style) # 논리적 ↔ 창의적
811
-
812
- # 자동 추천된 유머 스타일 업데이트
813
- refined_persona["유머스타일"] = humor_style
814
-
815
- # 127개 성격 변수가 있으면 업데이트
816
- if "성격변수127" in refined_persona:
817
- # 외향성 관련 변수 업데이트
818
- for var in ["E01_사교성", "E02_활동성", "E03_자기주장", "E06_열정성"]:
819
- if var in refined_persona["성격변수127"]:
820
- refined_persona["성격변수127"][var] = int(extraversion * 0.9 + random.randint(0, 20))
821
-
822
- # 감정표현 관련 변수 업데이트
823
- for var in ["W09_친밀감표현", "W06_공감능력", "E04_긍정정서"]:
824
- if var in refined_persona["성격변수127"]:
825
- refined_persona["성격변수127"][var] = int(emotion_expression * 0.9 + random.randint(0, 20))
826
-
827
- # 에너지 관련 변수 업데이트
828
- for var in ["E02_활동성", "E06_열정성", "E05_자극추구"]:
829
- if var in refined_persona["성격변수127"]:
830
- refined_persona["성격변수127"][var] = int(energy * 0.9 + random.randint(0, 20))
831
-
832
- # 사고방식 관련 변수 업데이트
833
- for var in ["C02_지능", "C06_분석력", "C01_효율성"]:
834
- if var in refined_persona["성격변수127"]:
835
- refined_persona["성격변수127"][var] = int(thinking_style * 0.9 + random.randint(0, 20))
836
-
837
- # 창의성 관련 변수 업데이트 (논리적 사고와 반대)
838
- for var in ["C04_창의성", "C08_통찰력"]:
839
- if var in refined_persona["성격변수127"]:
840
- refined_persona["성격변수127"][var] = int((100 - thinking_style) * 0.9 + random.randint(0, 20))
841
-
842
- # 유머 매트릭스 업데이트
843
- if "유머매트릭스" in refined_persona:
844
- if humor_style == "위트있는 재치꾼":
845
- refined_persona["유머매트릭스"]["warmth_vs_wit"] = 30
846
- refined_persona["유머매트릭스"]["self_vs_observational"] = 50
847
- refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 70
848
- elif humor_style == "따뜻한 유머러스":
849
- refined_persona["유머매트릭스"]["warmth_vs_wit"] = 80
850
- refined_persona["유머매트릭스"]["self_vs_observational"] = 60
851
- refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 60
852
- elif humor_style == "날카로운 관찰자":
853
- refined_persona["유머매트릭스"]["warmth_vs_wit"] = 40
854
- refined_persona["유머매트릭스"]["self_vs_observational"] = 20
855
- refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 50
856
- elif humor_style == "자기 비하적":
857
- refined_persona["유머매트릭스"]["warmth_vs_wit"] = 60
858
- refined_persona["유머매트릭스"]["self_vs_observational"] = 85
859
- refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 40
860
-
861
- return refined_persona, "성격이 성공적으로 미세조정되었습니다."
862
-
863
- except Exception as e:
864
- import traceback
865
- error_details = traceback.format_exc()
866
- print(f"성격 미세조정 오류: {error_details}")
867
- return persona, f"성격 미세조정 중 오류가 발생했습니다: {str(e)}"
868
-
869
- def create_frontend_view_html(persona):
870
- """Create HTML representation of the frontend view of the persona"""
871
- if not persona:
872
- return "<div class='persona-details'>페르소나가 아직 생성되지 않았습니다.</div>"
873
-
874
- name = persona.get("기본정보", {}).get("이름", "Unknown")
875
- object_type = persona.get("기본정보", {}).get("유형", "Unknown")
876
- description = persona.get("기본정보", {}).get("설명", "")
877
-
878
- # 성격 요약 가져오기
879
- personality_summary = persona.get("성격요약", {})
880
- summary_html = ""
881
- if personality_summary:
882
- summary_items = []
883
- for trait, value in personality_summary.items():
884
- if isinstance(value, (int, float)):
885
- trait_name = trait
886
- trait_value = value
887
- summary_items.append(f"• {trait_name}: {trait_value:.1f}%")
888
-
889
- if summary_items:
890
- summary_html = "<div class='summary-section'><h4>성격 요약</h4><ul>" + "".join([f"<li>{item}</li>" for item in summary_items]) + "</ul></div>"
891
-
892
- # Personality traits
893
- traits_html = ""
894
- for trait, value in persona.get("성격특성", {}).items():
895
- traits_html += f"""
896
- <div class="trait-item">
897
- <div class="trait-label">{trait}</div>
898
- <div class="trait-bar-container">
899
- <div class="trait-bar" style="width: {value}%; background: linear-gradient(90deg, #6366f1, #a5b4fc);"></div>
900
- </div>
901
- <div class="trait-value">{value}%</div>
902
- </div>
903
- """
904
-
905
- # Flaws - ���력적 결함
906
- flaws = persona.get("매력적결함", [])
907
- flaws_list = ""
908
- for flaw in flaws[:4]: # 최대 4개만 표시
909
- flaws_list += f"<li>{flaw}</li>"
910
-
911
- # 소통 방식
912
- communication_style = persona.get("소통방식", "")
913
-
914
- # 유머 스타일
915
- humor_style = persona.get("유머스타일", "")
916
-
917
- # 전체 HTML 스타일과 내용
918
- html = f"""
919
- <style>
920
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
921
-
922
- .frontend-persona {{
923
- font-family: 'Noto Sans KR', sans-serif;
924
- color: #333;
925
- max-width: 100%;
926
- }}
927
-
928
- .persona-header {{
929
- background: linear-gradient(135deg, #6366f1, #a5b4fc);
930
- padding: 20px;
931
- border-radius: 12px;
932
- color: white;
933
- margin-bottom: 20px;
934
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
935
- }}
936
-
937
- .persona-header h2 {{
938
- margin: 0;
939
- font-size: 24px;
940
- }}
941
-
942
- .persona-header p {{
943
- margin: 5px 0 0 0;
944
- opacity: 0.9;
945
- }}
946
-
947
- .persona-section {{
948
- background: #f8f9fa;
949
- border-radius: 8px;
950
- padding: 15px;
951
- margin-bottom: 15px;
952
- border: 1px solid #e0e0e0;
953
- }}
954
-
955
- .section-title {{
956
- font-size: 18px;
957
- margin: 0 0 10px 0;
958
- color: #444;
959
- border-bottom: 2px solid #6366f1;
960
- padding-bottom: 5px;
961
- display: inline-block;
962
- }}
963
-
964
- .trait-item {{
965
- display: flex;
966
- align-items: center;
967
- margin-bottom: 8px;
968
- }}
969
-
970
- .trait-label {{
971
- width: 80px;
972
- font-weight: 500;
973
- }}
974
-
975
- .trait-bar-container {{
976
- flex-grow: 1;
977
- background: #e0e0e0;
978
- height: 10px;
979
- border-radius: 5px;
980
- margin: 0 10px;
981
- overflow: hidden;
982
- }}
983
-
984
- .trait-bar {{
985
- height: 100%;
986
- border-radius: 5px;
987
- }}
988
-
989
- .trait-value {{
990
- width: 40px;
991
- text-align: right;
992
- font-size: 14px;
993
- }}
994
-
995
- .tags-container {{
996
- display: flex;
997
- flex-wrap: wrap;
998
- gap: 8px;
999
- margin-top: 10px;
1000
- }}
1001
-
1002
- .flaw-tag, .contradiction-tag, .interest-tag {{
1003
- background: #f0f4ff;
1004
- border: 1px solid #d0d4ff;
1005
- padding: 6px 12px;
1006
- border-radius: 16px;
1007
- font-size: 14px;
1008
- display: inline-block;
1009
- }}
1010
-
1011
- .flaw-tag {{
1012
- background: #fff0f0;
1013
- border-color: #ffd0d0;
1014
- }}
1015
-
1016
- .contradiction-tag {{
1017
- background: #f0fff4;
1018
- border-color: #d0ffd4;
1019
- }}
1020
-
1021
- /* 영혼 각성 UX 스타일 */
1022
- .awakening-result {{
1023
- background: #f9f9ff;
1024
- border-radius: 12px;
1025
- padding: 20px;
1026
- margin: 15px 0;
1027
- box-shadow: 0 4px 6px rgba(0,0,0,0.05);
1028
- border: 1px solid #e0e0e0;
1029
- }}
1030
-
1031
- .speech-bubble {{
1032
- background: #fff;
1033
- border-radius: 18px;
1034
- padding: 15px;
1035
- margin-bottom: 15px;
1036
- position: relative;
1037
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
1038
- border: 1px solid #e5e7eb;
1039
- }}
1040
-
1041
- .speech-bubble:after {{
1042
- content: '';
1043
- position: absolute;
1044
- bottom: -10px;
1045
- left: 30px;
1046
- border-width: 10px 10px 0;
1047
- border-style: solid;
1048
- border-color: #fff transparent;
1049
- }}
1050
-
1051
- .persona-speech {{
1052
- margin: 0;
1053
- font-size: 15px;
1054
- line-height: 1.5;
1055
- color: #4b5563;
1056
- }}
1057
-
1058
- .persona-traits-highlight {{
1059
- background: #f0f4ff;
1060
- border-radius: 10px;
1061
- padding: 15px;
1062
- margin: 15px 0;
1063
- }}
1064
-
1065
- .persona-traits-highlight h4 {{
1066
- margin-top: 0;
1067
- margin-bottom: 10px;
1068
- color: #4338ca;
1069
- }}
1070
-
1071
- .persona-traits-highlight ul {{
1072
- margin: 0;
1073
- padding-left: 20px;
1074
- color: #4b5563;
1075
- }}
1076
-
1077
- .persona-traits-highlight li {{
1078
- margin-bottom: 5px;
1079
- }}
1080
-
1081
- .first-interaction {{
1082
- margin-top: 20px;
1083
- }}
1084
-
1085
- .interaction-buttons, .confirmation-buttons {{
1086
- display: flex;
1087
- gap: 10px;
1088
- margin-top: 15px;
1089
- }}
1090
-
1091
- .interaction-btn, .confirmation-btn {{
1092
- background: #f3f4f6;
1093
- border: 1px solid #d1d5db;
1094
- padding: 8px 16px;
1095
- border-radius: 8px;
1096
- font-size: 14px;
1097
- cursor: pointer;
1098
- transition: all 0.2s;
1099
- font-family: 'Noto Sans KR', sans-serif;
1100
- }}
1101
-
1102
- .interaction-btn:hover, .confirmation-btn:hover {{
1103
- background: #e5e7eb;
1104
- }}
1105
-
1106
- .confirmation-btn.primary {{
1107
- background: #6366f1;
1108
- color: white;
1109
- border: 1px solid #4f46e5;
1110
- }}
1111
-
1112
- .confirmation-btn.primary:hover {{
1113
- background: #4f46e5;
1114
- }}
1115
-
1116
- /* 요약 섹션 스타일 */
1117
- .summary-section {{
1118
- background: #f0f4ff;
1119
- border-radius: 10px;
1120
- padding: 15px;
1121
- margin: 15px 0;
1122
- }}
1123
-
1124
- .summary-section h4 {{
1125
- margin-top: 0;
1126
- margin-bottom: 10px;
1127
- color: #4338ca;
1128
- }}
1129
-
1130
- .summary-section ul {{
1131
- margin: 0;
1132
- padding-left: 20px;
1133
- color: #4b5563;
1134
- }}
1135
-
1136
- .summary-section li {{
1137
- margin-bottom: 5px;
1138
- }}
1139
- </style>
1140
-
1141
- <div class="frontend-persona">
1142
- <div class="persona-header">
1143
- <h2>{name}</h2>
1144
- <p><strong>{object_type}</strong> - {description}</p>
1145
- </div>
1146
-
1147
- {summary_html}
1148
-
1149
- <div class="persona-section">
1150
- <h3 class="section-title">성격 특성</h3>
1151
- <div class="traits-container">
1152
- {traits_html}
1153
- </div>
1154
- </div>
1155
-
1156
- <div class="persona-section">
1157
- <h3 class="section-title">소통 스타일</h3>
1158
- <p>{communication_style}</p>
1159
- <h3 class="section-title" style="margin-top: 15px;">유머 스타일</h3>
1160
- <p>{humor_style}</p>
1161
- </div>
1162
-
1163
- <div class="persona-section">
1164
- <h3 class="section-title">매력적 결함</h3>
1165
- <ul class="flaws-list">
1166
- {flaws_list}
1167
- </ul>
1168
- </div>
1169
- </div>
1170
- """
1171
-
1172
- return html
1173
-
1174
- def create_backend_view_html(persona):
1175
- """Create HTML representation of the backend view of the persona"""
1176
- if not persona:
1177
- return "<div class='persona-details'>페르소나가 아직 생성되지 않았습니다.</div>"
1178
-
1179
- name = persona.get("기본정보", {}).get("이름", "Unknown")
1180
-
1181
- # 백엔드 기본 정보
1182
- basic_info = persona.get("기본정보", {})
1183
- basic_info_html = ""
1184
- for key, value in basic_info.items():
1185
- basic_info_html += f"<tr><td><strong>{key}</strong></td><td>{value}</td></tr>"
1186
-
1187
- # 1. 성격 변수 요약
1188
- personality_summary = persona.get("성격요약", {})
1189
- summary_html = ""
1190
-
1191
- if personality_summary:
1192
- summary_html += "<div class='summary-container'>"
1193
- for category, value in personality_summary.items():
1194
- if isinstance(value, (int, float)):
1195
- summary_html += f"""
1196
- <div class='summary-item'>
1197
- <div class='summary-label'>{category}</div>
1198
- <div class='summary-bar-container'>
1199
- <div class='summary-bar' style='width: {value}%; background: linear-gradient(90deg, #10b981, #6ee7b7);'></div>
1200
- </div>
1201
- <div class='summary-value'>{value:.1f}</div>
1202
- </div>
1203
- """
1204
- summary_html += "</div>"
1205
-
1206
- # 2. 성격 매트릭스 (5차원 빅5 시각화)
1207
- big5_html = ""
1208
- if "성격특성" in persona:
1209
- # 빅5 매핑 (기존 특성에서 변환)
1210
- big5 = {
1211
- "외향성(Extraversion)": persona.get("성격특성", {}).get("외향성", 50),
1212
- "친화성(Agreeableness)": persona.get("성격특성", {}).get("온기", 50),
1213
- "성실성(Conscientiousness)": persona.get("성격특성", {}).get("신뢰성", 50),
1214
- "신경증(Neuroticism)": 100 - persona.get("성격특성", {}).get("안정성", 50) if "안정성" in persona.get("성격특성", {}) else 50,
1215
- "개방성(Openness)": persona.get("성격특성", {}).get("창의성", 50)
1216
- }
1217
-
1218
- big5_html = "<div class='big5-matrix'>"
1219
- for trait, value in big5.items():
1220
- big5_html += f"""
1221
- <div class='big5-item'>
1222
- <div class='big5-label'>{trait}</div>
1223
- <div class='big5-bar-container'>
1224
- <div class='big5-bar' style='width: {value}%;'></div>
1225
- </div>
1226
- <div class='big5-value'>{value}%</div>
1227
- </div>
1228
- """
1229
- big5_html += "</div>"
1230
-
1231
- # 3. 유머 매트릭스
1232
- humor_matrix = persona.get("유머매트릭스", {})
1233
- humor_html = ""
1234
-
1235
- if humor_matrix:
1236
- warmth_vs_wit = humor_matrix.get("warmth_vs_wit", 50)
1237
- self_vs_observational = humor_matrix.get("self_vs_observational", 50)
1238
- subtle_vs_expressive = humor_matrix.get("subtle_vs_expressive", 50)
1239
-
1240
- humor_html = f"""
1241
- <div class='humor-matrix'>
1242
- <div class='humor-dimension'>
1243
- <div class='dimension-label'>따뜻함 vs 위트</div>
1244
- <div class='dimension-bar-container'>
1245
- <div class='dimension-indicator' style='left: {warmth_vs_wit}%;'></div>
1246
- <div class='dimension-label-left'>위트</div>
1247
- <div class='dimension-label-right'>따뜻함</div>
1248
- </div>
1249
- </div>
1250
-
1251
- <div class='humor-dimension'>
1252
- <div class='dimension-label'>자기참조 vs 관찰형</div>
1253
- <div class='dimension-bar-container'>
1254
- <div class='dimension-indicator' style='left: {self_vs_observational}%;'></div>
1255
- <div class='dimension-label-left'>관찰형</div>
1256
- <div class='dimension-label-right'>자기참조</div>
1257
- </div>
1258
- </div>
1259
-
1260
- <div class='humor-dimension'>
1261
- <div class='dimension-label'>미묘함 vs 표현적</div>
1262
- <div class='dimension-bar-container'>
1263
- <div class='dimension-indicator' style='left: {subtle_vs_expressive}%;'></div>
1264
- <div class='dimension-label-left'>미묘함</div>
1265
- <div class='dimension-label-right'>표현적</div>
1266
- </div>
1267
- </div>
1268
- </div>
1269
- """
1270
-
1271
- # 4. 매력적 결함과 모순적 특성
1272
- flaws_html = ""
1273
- contradictions_html = ""
1274
-
1275
- flaws = persona.get("매력적결함", [])
1276
- if flaws:
1277
- flaws_html = "<ul class='flaws-list'>"
1278
- for flaw in flaws:
1279
- flaws_html += f"<li>{flaw}</li>"
1280
- flaws_html += "</ul>"
1281
-
1282
- contradictions = persona.get("모순적특성", [])
1283
- if contradictions:
1284
- contradictions_html = "<ul class='contradictions-list'>"
1285
- for contradiction in contradictions:
1286
- contradictions_html += f"<li>{contradiction}</li>"
1287
- contradictions_html += "</ul>"
1288
-
1289
- # 6. 프롬프트 템플릿 (있는 경우)
1290
- prompt_html = ""
1291
- if "프롬프트" in persona:
1292
- prompt_text = persona.get("프롬프트", "")
1293
- prompt_html = f"""
1294
- <div class='prompt-section'>
1295
- <h3 class='section-title'>대화 프롬프트</h3>
1296
- <pre class='prompt-text'>{prompt_text}</pre>
1297
- </div>
1298
- """
1299
-
1300
- # 7. 완전한 백엔드 JSON (접이식)
1301
  try:
1302
- # 내부 상태 객체 제거 (JSON 변환 불가)
1303
- json_persona = {k: v for k, v in persona.items() if k not in ["personality_profile", "humor_matrix"]}
1304
- persona_json = json.dumps(json_persona, ensure_ascii=False, indent=2)
1305
-
1306
- json_preview = f"""
1307
- <details class='json-details'>
1308
- <summary>전체 백엔드 데이터 (JSON)</summary>
1309
- <pre class='json-preview'>{persona_json}</pre>
1310
- </details>
1311
- """
1312
- except Exception as e:
1313
- json_preview = f"<div class='error'>JSON 변환 오류: {str(e)}</div>"
1314
-
1315
- # 8. 전체 HTML 조합
1316
- html = f"""
1317
- <style>
1318
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
1319
-
1320
- .backend-persona {{
1321
- font-family: 'Noto Sans KR', sans-serif;
1322
- color: #333;
1323
- max-width: 100%;
1324
- }}
1325
-
1326
- .backend-header {{
1327
- background: linear-gradient(135deg, #059669, #34d399);
1328
- padding: 20px;
1329
- border-radius: 12px;
1330
- color: white;
1331
- margin-bottom: 20px;
1332
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1333
- }}
1334
-
1335
- .backend-header h2 {{
1336
- margin: 0;
1337
- font-size: 24px;
1338
- }}
1339
-
1340
- .backend-header p {{
1341
- margin: 5px 0 0 0;
1342
- opacity: 0.9;
1343
- }}
1344
-
1345
- .backend-section {{
1346
- background: #f8f9fa;
1347
- border-radius: 8px;
1348
- padding: 15px;
1349
- margin-bottom: 15px;
1350
- border: 1px solid #e0e0e0;
1351
- }}
1352
-
1353
- .section-title {{
1354
- font-size: 18px;
1355
- margin: 0 0 10px 0;
1356
- color: #444;
1357
- border-bottom: 2px solid #10b981;
1358
- padding-bottom: 5px;
1359
- display: inline-block;
1360
- }}
1361
-
1362
- /* 기본 정보 테이블 */
1363
- .basic-info-table {{
1364
- width: 100%;
1365
- border-collapse: collapse;
1366
- }}
1367
-
1368
- .basic-info-table td {{
1369
- padding: 8px;
1370
- border-bottom: 1px solid #e0e0e0;
1371
- }}
1372
-
1373
- .basic-info-table td:first-child {{
1374
- width: 120px;
1375
- font-weight: 500;
1376
- }}
1377
-
1378
- /* 요약 스타일 */
1379
- .summary-container {{
1380
- margin-top: 10px;
1381
- }}
1382
-
1383
- .summary-item {{
1384
- display: flex;
1385
- align-items: center;
1386
- margin-bottom: 8px;
1387
- }}
1388
-
1389
- .summary-label {{
1390
- width: 150px;
1391
- font-weight: 500;
1392
- }}
1393
-
1394
- .summary-bar-container {{
1395
- flex-grow: 1;
1396
- background: #e0e0e0;
1397
- height: 10px;
1398
- border-radius: 5px;
1399
- margin: 0 10px;
1400
- overflow: hidden;
1401
- }}
1402
-
1403
- .summary-bar {{
1404
- height: 100%;
1405
- border-radius: 5px;
1406
- }}
1407
-
1408
- .summary-value {{
1409
- width: 40px;
1410
- text-align: right;
1411
- font-size: 14px;
1412
- }}
1413
-
1414
- /* 빅5 성격 매트릭스 */
1415
- .big5-matrix {{
1416
- margin-top: 15px;
1417
- }}
1418
-
1419
- .big5-item {{
1420
- display: flex;
1421
- align-items: center;
1422
- margin-bottom: 12px;
1423
- }}
1424
-
1425
- .big5-label {{
1426
- width: 150px;
1427
- font-weight: 500;
1428
- }}
1429
-
1430
- .big5-bar-container {{
1431
- flex-grow: 1;
1432
- background: #e0e0e0;
1433
- height: 12px;
1434
- border-radius: 6px;
1435
- margin: 0 10px;
1436
- overflow: hidden;
1437
- }}
1438
-
1439
- .big5-bar {{
1440
- height: 100%;
1441
- border-radius: 6px;
1442
- background: linear-gradient(90deg, #10b981, #34d399);
1443
- }}
1444
-
1445
- .big5-value {{
1446
- width: 40px;
1447
- text-align: right;
1448
- font-weight: 500;
1449
- }}
1450
-
1451
- /* 유머 매트릭스 스타일 */
1452
- .humor-matrix {{
1453
- margin-top: 15px;
1454
- }}
1455
-
1456
- .humor-dimension {{
1457
- margin-bottom: 20px;
1458
- }}
1459
-
1460
- .dimension-label {{
1461
- font-weight: 500;
1462
- margin-bottom: 5px;
1463
- }}
1464
-
1465
- .dimension-bar-container {{
1466
- height: 20px;
1467
- background: #e0e0e0;
1468
- border-radius: 10px;
1469
- position: relative;
1470
- margin-top: 5px;
1471
- }}
1472
-
1473
- .dimension-indicator {{
1474
- width: 20px;
1475
- height: 20px;
1476
- background: #10b981;
1477
- border-radius: 50%;
1478
- position: absolute;
1479
- top: 0;
1480
- transform: translateX(-50%);
1481
- }}
1482
-
1483
- .dimension-label-left, .dimension-label-right {{
1484
- position: absolute;
1485
- top: -20px;
1486
- font-size: 12px;
1487
- color: #666;
1488
- }}
1489
-
1490
- .dimension-label-left {{
1491
- left: 10px;
1492
- }}
1493
-
1494
- .dimension-label-right {{
1495
- right: 10px;
1496
- }}
1497
-
1498
- /* 매력적 결함 및 모순적 특성 */
1499
- .flaws-list, .contradictions-list {{
1500
- margin: 0;
1501
- padding-left: 20px;
1502
- }}
1503
-
1504
- .flaws-list li, .contradictions-list li {{
1505
- margin-bottom: 6px;
1506
- }}
1507
-
1508
- /* 프롬프트 섹션 */
1509
- .prompt-text {{
1510
- background: #f3f4f6;
1511
- border-radius: 6px;
1512
- padding: 15px;
1513
- font-family: monospace;
1514
- white-space: pre-wrap;
1515
- font-size: 14px;
1516
- color: #374151;
1517
- max-height: 400px;
1518
- overflow-y: auto;
1519
- }}
1520
-
1521
- /* JSON 미리보기 스타일 */
1522
- .json-details {{
1523
- margin-top: 15px;
1524
- }}
1525
-
1526
- .json-details summary {{
1527
- cursor: pointer;
1528
- padding: 10px;
1529
- background: #f0f0f0;
1530
- border-radius: 5px;
1531
- font-weight: 500;
1532
- }}
1533
-
1534
- .json-preview {{
1535
- background: #f8f8f8;
1536
- padding: 15px;
1537
- border-radius: 5px;
1538
- border: 1px solid #ddd;
1539
- margin-top: 10px;
1540
- overflow-x: auto;
1541
- color: #333;
1542
- font-family: monospace;
1543
- font-size: 14px;
1544
- line-height: 1.5;
1545
- max-height: 400px;
1546
- overflow-y: auto;
1547
- }}
1548
-
1549
- .error {{
1550
- color: #e53e3e;
1551
- padding: 10px;
1552
- background: #fff5f5;
1553
- border-radius: 5px;
1554
- margin-top: 10px;
1555
- }}
1556
- </style>
1557
-
1558
- <div class="backend-persona">
1559
- <div class="backend-header">
1560
- <h2>{name} - 백엔드 데이터</h2>
1561
- <p>상세 정보와 내부 변수 확인</p>
1562
- </div>
1563
-
1564
- <div class="backend-section">
1565
- <h3 class="section-title">기본 정보</h3>
1566
- <table class="basic-info-table">
1567
- {basic_info_html}
1568
- </table>
1569
- </div>
1570
-
1571
- <div class="backend-section">
1572
- <h3 class="section-title">성격 요약 (Big 5)</h3>
1573
- {big5_html}
1574
- </div>
1575
-
1576
- <div class="backend-section">
1577
- <h3 class="section-title">유머 매트릭스 (3차원)</h3>
1578
- {humor_html}
1579
- </div>
1580
-
1581
- <div class="backend-section">
1582
- <h3 class="section-title">매력적 결함</h3>
1583
- {flaws_html}
1584
-
1585
- <h3 class="section-title" style="margin-top: 20px;">모순적 특성</h3>
1586
- {contradictions_html}
1587
- </div>
1588
-
1589
- {prompt_html}
1590
-
1591
- <div class="backend-section">
1592
- <h3 class="section-title">전체 백엔드 데이터</h3>
1593
- {json_preview}
1594
- </div>
1595
- </div>
1596
- """
1597
-
1598
- return html
1599
-
1600
- def get_personas_list():
1601
- """Get list of personas for the dataframe"""
1602
- personas = list_personas()
1603
-
1604
- # Convert to dataframe format
1605
- df_data = []
1606
- for i, persona in enumerate(personas):
1607
- df_data.append([
1608
- persona["name"],
1609
- persona["type"],
1610
- persona["created_at"],
1611
- persona["filename"]
1612
- ])
1613
-
1614
- return df_data, personas
1615
-
1616
- def load_selected_persona(selected_row, personas_list):
1617
- """Load persona from the selected row in the dataframe"""
1618
  if selected_row is None or len(selected_row) == 0:
1619
- return None, "선택된 페르소나가 없습니다.", None, None, None
1620
 
1621
  try:
1622
- # Get filepath from selected row
1623
  selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0
 
 
 
1624
  filepath = personas_list[selected_index]["filepath"]
1625
-
1626
- # Load persona
1627
  persona = load_persona(filepath)
 
1628
  if not persona:
1629
- return None, "페르소나 로딩에 실패했습니다.", None, None, None
 
 
 
 
 
 
 
 
 
1630
 
1631
- # Generate HTML views
1632
- frontend_view, backend_view = toggle_frontend_backend_view(persona)
1633
- frontend_html = create_frontend_view_html(frontend_view)
1634
- backend_html = create_backend_view_html(backend_view)
1635
 
1636
- # Generate personality chart
1637
- chart_image_path = generate_personality_chart(frontend_view)
 
 
 
 
 
 
 
 
 
 
 
1638
 
1639
- return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image_path
1640
 
1641
  except Exception as e:
1642
- return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None
1643
 
1644
- # 페르소나와 대화하는 함수 추가
1645
- def chat_with_persona(persona, user_message, chat_history=None):
1646
- """
1647
- 페르소나와 대화하는 함수
1648
- """
1649
  if chat_history is None:
1650
  chat_history = []
1651
 
@@ -1653,358 +309,176 @@ def chat_with_persona(persona, user_message, chat_history=None):
1653
  return chat_history, ""
1654
 
1655
  if not persona:
1656
- # Gradio 4.x 호환 메시지 형식 (튜플)
1657
- chat_history.append([user_message, "페르소나가 로드되지 않았습니다. 먼저 페르소나를 생성하거나 불러오세요."])
1658
  return chat_history, ""
1659
 
1660
  try:
1661
- # 페르소나 생성기에서 대화 기능 호출
1662
- # 이전 대화 기록 변환 필요 - 리스트에서 튜플 형식으로
1663
- converted_history = []
1664
- for msg in chat_history:
1665
- if isinstance(msg, list) and len(msg) == 2:
1666
- # 리스트 형식이면 튜플로 변환
1667
- converted_history.append((msg[0] if msg[0] else "", msg[1] if msg[1] else ""))
1668
- elif isinstance(msg, tuple) and len(msg) == 2:
1669
- # 이미 튜플 형식이면 그대로 사용
1670
- converted_history.append(msg)
1671
-
1672
- # 페르소나 생성기에서 대화 함수 호출
1673
- response = persona_generator.chat_with_persona(persona, user_message, converted_history)
1674
-
1675
- # Gradio 4.x 메시지 형식으로 추가 (리스트)
1676
  chat_history.append([user_message, response])
1677
-
1678
  return chat_history, ""
1679
  except Exception as e:
1680
- import traceback
1681
- error_details = traceback.format_exc()
1682
- print(f"대화 오류: {error_details}")
1683
  chat_history.append([user_message, f"대화 중 오류가 발생했습니다: {str(e)}"])
1684
  return chat_history, ""
1685
 
1686
- # 메인 Gradio 인터페이스 구성 함수
1687
- def create_interface():
1688
- # 현재 persona 상태 저장 - Gradio 5.x에서 변경된 방식 적용
1689
  current_persona = gr.State(value=None)
1690
  personas_list = gr.State(value=[])
1691
 
1692
- with gr.Blocks(theme=theme, css=css) as app:
1693
  gr.Markdown("""
1694
  # 놈팽쓰(MemoryTag): 당신 곁의 사물, 이제 친구가 되다
1695
- 이 데모는 일상 속 사물에 AI 페르소나를 부여하여 대화할 수 있게 해주는 서비스입니다.
1696
  """)
1697
 
1698
  with gr.Tabs() as tabs:
1699
- with gr.Tab("페르소나 생성", id="persona_creation"):
 
1700
  with gr.Row():
1701
  with gr.Column(scale=1):
1702
- # 이미지 업로드 영역
1703
- image_input = gr.Image(
1704
- type="pil",
1705
- width=300,
1706
- height=300,
1707
- label="사물 이미지를 업로드하세요"
1708
- )
1709
- # 입력 필드들
1710
  with gr.Group():
1711
- gr.Markdown("### 맥락 정보 입력")
1712
- name_input = gr.Textbox(label="사물 이름 (빈칸일 경우 자동 생성)", placeholder="예: 책상 위 램프")
1713
-
1714
  location_input = gr.Dropdown(
1715
  choices=["집", "사무실", "여행 중", "상점", "학교", "카페", "기타"],
1716
  label="주로 어디에 있나요?",
1717
  value="집"
1718
  )
1719
-
1720
  time_spent_input = gr.Dropdown(
1721
  choices=["새것", "몇 개월", "1년 이상", "오래됨", "중고/빈티지"],
1722
  label="얼마나 함께했나요?",
1723
  value="몇 개월"
1724
  )
1725
-
1726
  object_type_input = gr.Dropdown(
1727
  choices=["가전제품", "가구", "전자기기", "장식품", "도구", "개인용품", "기타"],
1728
  label="어떤 종류의 사물인가요?",
1729
  value="가구"
1730
  )
1731
 
1732
- # 사용자 입력들 상태 저장 - Gradio 5.x에서 변경된 방식 적용
1733
- user_inputs = gr.State(value={})
1734
-
1735
- with gr.Row():
1736
- discover_btn = gr.Button("1. 영혼 발견하기", variant="primary")
1737
- create_btn = gr.Button("2. 페르소나 생성", variant="secondary")
1738
-
1739
- # 영혼 깨우기 결과 표시 영역
1740
- awakening_output = gr.HTML(visible=False)
1741
- error_output = gr.Markdown(visible=False)
1742
 
1743
  with gr.Column(scale=1):
1744
- # 이미지 분석 결과
1745
- image_analysis_output = gr.JSON(label="이미지 분석 결과", visible=False)
1746
- # 페르소나 기본 정보 및 특성
1747
  basic_info_output = gr.JSON(label="기본 정보")
1748
- personality_traits_output = gr.JSON(label="페르소나 특성")
1749
 
1750
- # 페르소나 저장 및 내보내기 버튼
1751
  with gr.Row():
1752
- save_btn = gr.Button("페르소나 저장", variant="primary")
1753
- download_btn = gr.Button("JSON으로 내보내기", variant="secondary")
1754
-
1755
- # 성향 미세조정
1756
- with gr.Accordion("성향 미세조정", open=False):
1757
- with gr.Row():
1758
- with gr.Column(scale=1):
1759
- warmth_slider = gr.Slider(0, 100, label="온기", step=1)
1760
- competence_slider = gr.Slider(0, 100, label="능력", step=1)
1761
- creativity_slider = gr.Slider(0, 100, label="창의성", step=1)
1762
- with gr.Column(scale=1):
1763
- extraversion_slider = gr.Slider(0, 100, label="외향성", step=1)
1764
- humor_slider = gr.Slider(0, 100, label="유머감각", step=1)
1765
- trust_slider = gr.Slider(0, 100, label="신뢰도", step=1)
1766
-
1767
- humor_style = gr.Dropdown(
1768
- choices=["witty_wordsmith", "warm_humorist", "playful_trickster", "sharp_observer", "self_deprecating"],
1769
- label="유머 스타일",
1770
- value="warm_humorist"
1771
- )
1772
- apply_traits_btn = gr.Button("성향 적용하기")
1773
-
1774
- # 유머 스타일 시각화
1775
- humor_chart_output = gr.Plot(label="유머 스타일 매트릭스")
1776
-
1777
- # 페르소나 다운로드 관련 출력
1778
- json_output = gr.Textbox(label="JSON 데이터", visible=False)
1779
- download_output = gr.File(label="다운로드", visible=False)
1780
-
1781
- with gr.Tab("세부 정보", id="persona_details"):
1782
  with gr.Row():
1783
- with gr.Column(scale=1):
1784
- # 매력적 결함 데이터프레임
1785
- attractive_flaws_df_output = gr.Dataframe(
1786
  headers=["매력적 결함", "효과"],
1787
  label="매력적 결함",
1788
  interactive=False
1789
  )
1790
-
1791
- # 모순적 특성 데이터프레임
1792
- contradictions_df_output = gr.Dataframe(
1793
  headers=["모순적 특성", "효과"],
1794
  label="모순적 특성",
1795
  interactive=False
1796
  )
1797
 
1798
- with gr.Column(scale=1):
1799
- # 성격 차트
1800
- personality_chart_output = gr.Plot(label="성격 차트")
1801
 
1802
- # 127개 성격 변수 데이터프레임
1803
- with gr.Accordion("127개 성격 변수 세부정보", open=False):
1804
- personality_variables_df_output = gr.Dataframe(
1805
  headers=["변수", "값", "설명"],
1806
- label="성격 변수 (127개)",
1807
  interactive=False
1808
  )
1809
 
1810
- with gr.Tab("대화하기", id="persona_chat"):
 
1811
  with gr.Row():
1812
  with gr.Column(scale=1):
1813
- # 페르소나 불러오기 기능
1814
  gr.Markdown("### 페르소나 불러오기")
1815
-
1816
- with gr.Row():
1817
- with gr.Column(scale=1):
1818
- # 저장된 페르소나 목록
1819
- refresh_personas_btn = gr.Button("목록 새로고침", variant="secondary")
1820
- persona_table = gr.Dataframe(
1821
- headers=["ID", "이름", "유형", "생성 날짜"],
1822
- label="저장된 페르소나",
1823
- interactive=False
1824
- )
1825
- load_persona_btn = gr.Button("선택한 페르소나 불러오기", variant="primary")
1826
-
1827
- with gr.Column(scale=1):
1828
- # JSON 파일에서 불러오기
1829
- gr.Markdown("### 또는 JSON 파일에서 불러오기")
1830
- json_upload = gr.File(
1831
- label="페르소나 JSON 파일 업로드",
1832
- file_types=[".json"]
1833
- )
1834
- import_persona_btn = gr.Button("JSON에서 가져오기", variant="primary")
1835
- import_status = gr.Markdown("")
1836
-
1837
  with gr.Column(scale=1):
1838
- # 현재 로드된 페르소나 정보
1839
- chat_persona_info = gr.Markdown("### 페르소나를 불러와 대화를 시작하세요")
1840
-
1841
- # 대화 인터페이스
1842
  chatbot = gr.Chatbot(height=400, label="대화")
1843
  with gr.Row():
1844
  message_input = gr.Textbox(
1845
  placeholder="메시지를 입력하세요...",
1846
- label="메시지",
1847
- show_label=False,
1848
  lines=2
1849
  )
1850
  send_btn = gr.Button("전송", variant="primary")
1851
 
1852
- # 영혼 깨우기 버튼 이벤트
1853
- discover_btn.click(
1854
- fn=lambda name, location, time_spent, object_type: {"name": name, "location": location, "time_spent": time_spent, "object_type": object_type},
1855
- inputs=[name_input, location_input, time_spent_input, object_type_input],
1856
- outputs=[user_inputs],
1857
- queue=False
1858
- ).then(
1859
- fn=show_awakening_progress,
1860
- inputs=[image_input, user_inputs],
1861
- outputs=[current_persona, error_output, awakening_output],
1862
- queue=True
1863
- )
1864
-
1865
- # 페르소나 생성 버튼 이벤트
1866
  create_btn.click(
1867
- fn=lambda name, location, time_spent, object_type: {"name": name, "location": location, "time_spent": time_spent, "object_type": object_type},
1868
- inputs=[name_input, location_input, time_spent_input, object_type_input],
1869
- outputs=[user_inputs],
1870
- queue=False
1871
- ).then(
1872
  fn=create_persona_from_image,
1873
- inputs=[image_input, user_inputs],
1874
  outputs=[
1875
- current_persona, error_output, image_input, image_analysis_output,
1876
- basic_info_output, personality_traits_output, humor_chart_output,
1877
- attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output
1878
- ],
1879
- queue=True
1880
- ).then(
1881
- fn=generate_personality_chart,
1882
- inputs=[current_persona],
1883
- outputs=[personality_chart_output]
1884
- ).then(
1885
- fn=lambda: gr.update(visible=False),
1886
- outputs=[awakening_output]
1887
- ).then(
1888
- fn=lambda persona: [
1889
- 50, 50, 50, 50, 50, 50 # 기본값
1890
- ],
1891
- inputs=[current_persona],
1892
- outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider]
1893
- )
1894
-
1895
- # 성향 미세조정 이벤트
1896
- apply_traits_btn.click(
1897
- fn=refine_persona,
1898
- inputs=[
1899
- current_persona, warmth_slider, competence_slider, creativity_slider,
1900
- extraversion_slider, humor_slider, trust_slider, humor_style
1901
- ],
1902
- outputs=[
1903
- current_persona, basic_info_output, personality_traits_output,
1904
- humor_chart_output, personality_chart_output, personality_variables_df_output
1905
  ]
1906
  )
1907
 
1908
- # 페르소나 저장 버튼 이벤트
1909
  save_btn.click(
1910
- fn=save_current_persona,
1911
  inputs=[current_persona],
1912
- outputs=[error_output]
1913
  )
1914
 
1915
- # 페르소나 JSON 내보내기 버튼 이벤트
1916
- download_btn.click(
1917
- fn=export_persona_json,
1918
  inputs=[current_persona],
1919
- outputs=[download_output, json_output]
1920
- ).then(
1921
- fn=lambda x: gr.update(visible=True if x else False),
1922
- inputs=[download_output],
1923
- outputs=[download_output]
1924
- ).then(
1925
- fn=lambda x: gr.update(visible=False),
1926
- inputs=[json_output],
1927
- outputs=[json_output]
1928
  )
1929
 
1930
- # 저장된 페르소나 목록 새로고침 이벤트
1931
- refresh_personas_btn.click(
1932
- fn=get_personas_list,
1933
  outputs=[persona_table, personas_list]
1934
  )
1935
 
1936
- # 저장된 페르소나 불러오기 이벤트
1937
- load_persona_btn.click(
1938
- fn=load_selected_persona,
1939
  inputs=[persona_table, personas_list],
1940
  outputs=[
1941
- current_persona, chat_persona_info, chatbot,
1942
- basic_info_output, personality_traits_output, humor_chart_output,
1943
- attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output
1944
- ]
1945
- ).then(
1946
- fn=generate_personality_chart,
1947
- inputs=[current_persona],
1948
- outputs=[personality_chart_output]
1949
- ).then(
1950
- fn=lambda persona: [50, 50, 50, 50, 50, 50], # 기본값
1951
- inputs=[current_persona],
1952
- outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider]
1953
- ).then(
1954
- fn=lambda: gr.update(selected="persona_creation"),
1955
- outputs=[tabs]
1956
- )
1957
-
1958
- # JSON에서 페르소나 가져오기 이벤트
1959
- import_persona_btn.click(
1960
- fn=import_persona_json,
1961
- inputs=[json_upload],
1962
- outputs=[current_persona, import_status]
1963
- ).then(
1964
- fn=lambda persona: update_current_persona_info(persona) if persona else (None, None, None, [], [], []),
1965
- inputs=[current_persona],
1966
- outputs=[
1967
- basic_info_output, personality_traits_output, humor_chart_output,
1968
- attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output
1969
  ]
1970
  ).then(
1971
  fn=generate_personality_chart,
1972
  inputs=[current_persona],
1973
  outputs=[personality_chart_output]
1974
- ).then(
1975
- fn=lambda persona: [50, 50, 50, 50, 50, 50], # 기본값
1976
- inputs=[current_persona],
1977
- outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider]
1978
- ).then(
1979
- fn=lambda persona: f"### 페르소나를 불러왔습니다" if persona else "### 페르소나를 불러오지 못했습니다",
1980
- inputs=[current_persona],
1981
- outputs=[chat_persona_info]
1982
- ).then(
1983
- fn=lambda: gr.update(selected="persona_creation"),
1984
- outputs=[tabs]
1985
  )
1986
 
1987
- # 메시지 전송 이벤트
1988
  send_btn.click(
1989
- fn=chat_with_persona,
1990
  inputs=[current_persona, message_input, chatbot],
1991
  outputs=[chatbot, message_input]
1992
  )
 
1993
  message_input.submit(
1994
- fn=chat_with_persona,
1995
  inputs=[current_persona, message_input, chatbot],
1996
  outputs=[chatbot, message_input]
1997
  )
1998
 
1999
- # 앱 로드 시 저장된 페르소나 목록 로드
2000
  app.load(
2001
- fn=get_personas_list,
2002
  outputs=[persona_table, personas_list]
2003
  )
2004
 
2005
  return app
2006
 
2007
- # 메인 실행 부분
2008
  if __name__ == "__main__":
2009
- app = create_interface()
2010
  app.launch(server_name="0.0.0.0", server_port=7860)
 
30
  export_persona_json, import_persona_json
31
  )
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  # Load environment variables
34
  load_dotenv()
35
 
 
38
  if api_key:
39
  genai.configure(api_key=api_key)
40
 
41
+ # Create data directories
42
  os.makedirs("data/personas", exist_ok=True)
43
  os.makedirs("data/conversations", exist_ok=True)
44
 
 
51
  secondary_hue="blue",
52
  )
53
 
54
+ # CSS styling
55
  css = """
 
56
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
57
 
58
  body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option {
59
  font-family: 'Noto Sans KR', sans-serif !important;
60
  }
61
 
 
 
 
 
 
 
62
  .persona-details {
63
  border: 1px solid #e0e0e0;
64
  border-radius: 8px;
65
  padding: 16px;
66
  margin-top: 12px;
67
  background-color: #f8f9fa;
68
+ color: #333333;
69
  }
70
 
71
  .awakening-container {
 
92
  border-radius: 4px;
93
  transition: width 0.5s ease-in-out;
94
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  """
96
 
97
+ # Variable descriptions
98
+ VARIABLE_DESCRIPTIONS = {
99
+ "W01_친절함": "타인을 돕고 배려하는 표현 빈도",
100
+ "W02_친근함": "접근하기 쉽고 개방적인 태도",
101
+ "W03_진실성": "솔직하고 정직한 표현 정도",
102
+ "C01_효율성": "과제 완수 능력과 반응 속도",
103
+ "C02_지능": "문제 해결과 논리적 사고 능력",
104
+ "E01_사교성": "타인과의 상호작용을 즐기는 정도",
 
 
 
 
 
105
  }
106
 
107
+ # Humor style mapping
108
  HUMOR_STYLE_MAPPING = {
109
  "Witty Wordsmith": "witty_wordsmith",
110
  "Warm Humorist": "warm_humorist",
 
112
  "Self-deprecating": "self_deprecating"
113
  }
114
 
115
+ def create_persona_from_image(image, name, location, time_spent, object_type, progress=gr.Progress()):
116
+ """페르소나 생성 함수"""
117
+ if image is None:
118
+ return None, "이미지를 업로드해주세요.", {}, {}, None, [], [], []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ progress(0.1, desc="이미지 분석 중...")
 
 
121
 
122
+ user_context = {
123
+ "name": name,
124
+ "location": location,
125
+ "time_spent": time_spent,
126
+ "object_type": object_type
127
+ }
128
 
129
+ try:
130
+ generator = PersonaGenerator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ progress(0.3, desc="이미지 분석 중...")
133
+ image_analysis = generator.analyze_image(image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ if object_type:
136
+ image_analysis["object_type"] = object_type
137
+
138
+ progress(0.6, desc="페르소나 생성 중...")
139
+ frontend_persona = generator.create_frontend_persona(image_analysis, user_context)
140
+
141
+ progress(0.8, desc="상세 페르소나 생성 중...")
142
+ backend_persona = generator.create_backend_persona(frontend_persona, image_analysis)
143
+
144
+ progress(1.0, desc="완료!")
145
+
146
+ # 기본 정보 추출
147
+ basic_info = {
148
+ "이름": backend_persona.get("기본정보", {}).get("이름", "Unknown"),
149
+ "유형": backend_persona.get("기본정보", {}).get("유형", "Unknown"),
150
+ "설명": backend_persona.get("기본정보", {}).get("설명", "")
151
+ }
152
+
153
+ personality_traits = backend_persona.get("성격특성", {})
154
+ humor_chart = plot_humor_matrix(backend_persona.get("유머매트릭스", {}))
155
+
156
+ attractive_flaws_df = []
157
+ contradictions_df = []
158
+ personality_variables_df = []
159
+
160
+ if "매력적결함" in backend_persona:
161
+ flaws = backend_persona["매력적결함"]
162
+ attractive_flaws_df = [[flaw, "매력 증가"] for flaw in flaws]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ if "모순적특성" in backend_persona:
165
+ contradictions = backend_persona["모순적특성"]
166
+ contradictions_df = [[contradiction, "복잡성 증가"] for contradiction in contradictions]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
+ if "성격변수127" in backend_persona:
169
+ variables = backend_persona["성격변수127"]
170
+ if isinstance(variables, dict):
171
+ personality_variables_df = [[var_name, score, VARIABLE_DESCRIPTIONS.get(var_name, "")]
172
+ for var_name, score in variables.items()]
173
+
174
+ return backend_persona, "페르소나 생성 완료!", basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
175
+
176
+ except Exception as e:
177
+ print(f"페르소나 생성 오류: {str(e)}")
178
+ return None, f"오류 발생: {str(e)}", {}, {}, None, [], [], []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  def generate_personality_chart(persona):
181
+ """성격 차트 생성"""
182
  if not persona or "성격특성" not in persona:
 
183
  img = Image.new('RGB', (400, 400), color='white')
184
  draw = PIL.ImageDraw.Draw(img)
185
  draw.text((150, 180), "No data", fill='black')
186
  img_path = os.path.join("data", "temp_chart.png")
187
+ os.makedirs("data", exist_ok=True)
188
  img.save(img_path)
189
  return img_path
190
 
 
191
  traits = persona["성격특성"]
192
+ categories = list(traits.keys())
193
+ values = list(traits.values())
194
 
195
+ # 차트 생성
196
+ fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ angles = np.linspace(0, 2*np.pi, len(categories), endpoint=False)
199
+ values_plot = values + [values[0]] # Close the plot
200
+ angles_plot = np.concatenate([angles, [angles[0]]])
201
 
202
+ ax.plot(angles_plot, values_plot, 'o-', linewidth=2, color='#6366f1')
203
+ ax.fill(angles_plot, values_plot, alpha=0.25, color='#6366f1')
204
+ ax.set_xticks(angles)
205
+ ax.set_xticklabels(categories)
 
206
  ax.set_ylim(0, 100)
207
 
208
+ plt.title("성격 특성", size=16, pad=20)
 
 
 
 
 
 
 
 
209
 
 
 
 
 
 
 
 
 
210
  timestamp = int(time.time())
211
  img_path = os.path.join("data", f"chart_{timestamp}.png")
212
+ plt.savefig(img_path, format='png', bbox_inches='tight', dpi=150)
 
213
  plt.close(fig)
214
 
215
  return img_path
216
 
217
+ def save_persona_to_file(persona):
218
+ """페르소나 저장"""
219
+ if not persona:
220
  return "저장할 페르소나가 없습니다."
221
 
222
  try:
223
+ persona_copy = copy.deepcopy(persona)
 
 
224
 
225
  # 저장 불가능한 객체 제거
226
+ for key in list(persona_copy.keys()):
227
+ if callable(persona_copy[key]):
228
+ persona_copy.pop(key, None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
+ filepath = save_persona(persona_copy)
231
  if filepath:
232
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
233
+ return f"{name} 페르소나가 저장되었습니다."
234
  else:
235
  return "페르소나 저장에 실패했습니다."
236
  except Exception as e:
 
 
 
237
  return f"저장 중 오류 발생: {str(e)}"
238
 
239
+ def get_saved_personas():
240
+ """저장된 페르소나 목록 가져오기"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  try:
242
+ personas = list_personas()
243
+ df_data = []
244
+ for i, persona in enumerate(personas):
245
+ df_data.append([
246
+ i,
247
+ persona["name"],
248
+ persona["type"],
249
+ persona["created_at"]
250
+ ])
251
+ return df_data, personas
252
+ except Exception:
253
+ return [], []
254
+
255
+ def load_persona_from_selection(selected_row, personas_list):
256
+ """선택된 페르소나 로드"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  if selected_row is None or len(selected_row) == 0:
258
+ return None, "선택된 페르소나�� 없습니다.", {}, {}, None, [], [], []
259
 
260
  try:
 
261
  selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0
262
+ if selected_index >= len(personas_list):
263
+ return None, "잘못된 선택입니다.", {}, {}, None, [], [], []
264
+
265
  filepath = personas_list[selected_index]["filepath"]
 
 
266
  persona = load_persona(filepath)
267
+
268
  if not persona:
269
+ return None, "페르소나 로딩에 실패했습니다.", {}, {}, None, [], [], []
270
+
271
+ basic_info = {
272
+ "이름": persona.get("기본정보", {}).get("이름", "Unknown"),
273
+ "유형": persona.get("기본정보", {}).get("유형", "Unknown"),
274
+ "설명": persona.get("기본정보", {}).get("설명", "")
275
+ }
276
+
277
+ personality_traits = persona.get("성격특성", {})
278
+ humor_chart = plot_humor_matrix(persona.get("유머매트릭스", {}))
279
 
280
+ attractive_flaws_df = []
281
+ contradictions_df = []
282
+ personality_variables_df = []
 
283
 
284
+ if "매력적결함" in persona:
285
+ flaws = persona["매력적결함"]
286
+ attractive_flaws_df = [[flaw, "매력 증가"] for flaw in flaws]
287
+
288
+ if "모순적특성" in persona:
289
+ contradictions = persona["모순적특성"]
290
+ contradictions_df = [[contradiction, "복잡성 증가"] for contradiction in contradictions]
291
+
292
+ if "성격변수127" in persona:
293
+ variables = persona["성격변수127"]
294
+ if isinstance(variables, dict):
295
+ personality_variables_df = [[var_name, score, VARIABLE_DESCRIPTIONS.get(var_name, "")]
296
+ for var_name, score in variables.items()]
297
 
298
+ return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
299
 
300
  except Exception as e:
301
+ return None, f"로딩 중 오류 발생: {str(e)}", {}, {}, None, [], [], []
302
 
303
+ def chat_with_loaded_persona(persona, user_message, chat_history=None):
304
+ """페르소나와 대화"""
 
 
 
305
  if chat_history is None:
306
  chat_history = []
307
 
 
309
  return chat_history, ""
310
 
311
  if not persona:
312
+ chat_history.append([user_message, "페르소나가 로드되지 않았습니다."])
 
313
  return chat_history, ""
314
 
315
  try:
316
+ response = persona_generator.chat_with_persona(persona, user_message, chat_history)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  chat_history.append([user_message, response])
 
318
  return chat_history, ""
319
  except Exception as e:
 
 
 
320
  chat_history.append([user_message, f"대화 중 오류가 발생했습니다: {str(e)}"])
321
  return chat_history, ""
322
 
323
+ # 메인 인터페이스 생성
324
+ def create_main_interface():
 
325
  current_persona = gr.State(value=None)
326
  personas_list = gr.State(value=[])
327
 
328
+ with gr.Blocks(theme=theme, css=css, title="놈팽쓰(MemoryTag)") as app:
329
  gr.Markdown("""
330
  # 놈팽쓰(MemoryTag): 당신 곁의 사물, 이제 친구가 되다
331
+ 일상 속 사물에 AI 페르소나를 부여하여 대화할 수 있게 해주는 서비스입니다.
332
  """)
333
 
334
  with gr.Tabs() as tabs:
335
+ # 페르소나 생성
336
+ with gr.Tab("페르소나 생성", id="creation"):
337
  with gr.Row():
338
  with gr.Column(scale=1):
339
+ image_input = gr.Image(type="pil", label="사물 이미지 업로드")
340
+
 
 
 
 
 
 
341
  with gr.Group():
342
+ gr.Markdown("### 기본 정보")
343
+ name_input = gr.Textbox(label="사물 이름 (선택사항)", placeholder="예: 책상 위 램프")
 
344
  location_input = gr.Dropdown(
345
  choices=["집", "사무실", "여행 중", "상점", "학교", "카페", "기타"],
346
  label="주로 어디에 있나요?",
347
  value="집"
348
  )
 
349
  time_spent_input = gr.Dropdown(
350
  choices=["새것", "몇 개월", "1년 이상", "오래됨", "중고/빈티지"],
351
  label="얼마나 함께했나요?",
352
  value="몇 개월"
353
  )
 
354
  object_type_input = gr.Dropdown(
355
  choices=["가전제품", "가구", "전자기기", "장식품", "도구", "개인용품", "기타"],
356
  label="어떤 종류의 사물인가요?",
357
  value="가구"
358
  )
359
 
360
+ create_btn = gr.Button("페르소나 생성", variant="primary", size="lg")
361
+ status_output = gr.Markdown("")
 
 
 
 
 
 
 
 
362
 
363
  with gr.Column(scale=1):
 
 
 
364
  basic_info_output = gr.JSON(label="기본 정보")
365
+ personality_traits_output = gr.JSON(label="성격 특성")
366
 
 
367
  with gr.Row():
368
+ save_btn = gr.Button("페르소나 저장", variant="secondary")
369
+ chart_btn = gr.Button("성격 차트 생성", variant="secondary")
370
+
371
+ # 상세 정보 탭
372
+ with gr.Tab("상세 정보", id="details"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  with gr.Row():
374
+ with gr.Column():
375
+ attractive_flaws_output = gr.Dataframe(
 
376
  headers=["매력적 결함", "효과"],
377
  label="매력적 결함",
378
  interactive=False
379
  )
380
+ contradictions_output = gr.Dataframe(
 
 
381
  headers=["모순적 특성", "효과"],
382
  label="모순적 특성",
383
  interactive=False
384
  )
385
 
386
+ with gr.Column():
387
+ personality_chart_output = gr.Image(label="성격 차트")
388
+ humor_chart_output = gr.Plot(label="유머 매트릭스")
389
 
390
+ with gr.Accordion("127개 성격 변수", open=False):
391
+ personality_variables_output = gr.Dataframe(
 
392
  headers=["변수", "값", "설명"],
393
+ label="성격 변수",
394
  interactive=False
395
  )
396
 
397
+ # 대화하기
398
+ with gr.Tab("대화하기", id="chat"):
399
  with gr.Row():
400
  with gr.Column(scale=1):
 
401
  gr.Markdown("### 페르소나 불러오기")
402
+ refresh_btn = gr.Button("목록 새로고침", variant="secondary")
403
+ persona_table = gr.Dataframe(
404
+ headers=["ID", "이름", "유형", "생성날짜"],
405
+ label="저장된 페르소나",
406
+ interactive=False
407
+ )
408
+ load_btn = gr.Button("선택한 페르소나 불러오기", variant="primary")
409
+ load_status = gr.Markdown("")
410
+
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  with gr.Column(scale=1):
412
+ gr.Markdown("### 대화")
 
 
 
413
  chatbot = gr.Chatbot(height=400, label="대화")
414
  with gr.Row():
415
  message_input = gr.Textbox(
416
  placeholder="메시지를 입력하세요...",
417
+ show_label=False,
 
418
  lines=2
419
  )
420
  send_btn = gr.Button("전송", variant="primary")
421
 
422
+ # 이벤트 핸들러
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  create_btn.click(
 
 
 
 
 
424
  fn=create_persona_from_image,
425
+ inputs=[image_input, name_input, location_input, time_spent_input, object_type_input],
426
  outputs=[
427
+ current_persona, status_output, basic_info_output, personality_traits_output,
428
+ humor_chart_output, attractive_flaws_output, contradictions_output, personality_variables_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  ]
430
  )
431
 
 
432
  save_btn.click(
433
+ fn=save_persona_to_file,
434
  inputs=[current_persona],
435
+ outputs=[status_output]
436
  )
437
 
438
+ chart_btn.click(
439
+ fn=generate_personality_chart,
 
440
  inputs=[current_persona],
441
+ outputs=[personality_chart_output]
 
 
 
 
 
 
 
 
442
  )
443
 
444
+ refresh_btn.click(
445
+ fn=get_saved_personas,
 
446
  outputs=[persona_table, personas_list]
447
  )
448
 
449
+ load_btn.click(
450
+ fn=load_persona_from_selection,
 
451
  inputs=[persona_table, personas_list],
452
  outputs=[
453
+ current_persona, load_status, basic_info_output, personality_traits_output,
454
+ humor_chart_output, attractive_flaws_output, contradictions_output, personality_variables_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  ]
456
  ).then(
457
  fn=generate_personality_chart,
458
  inputs=[current_persona],
459
  outputs=[personality_chart_output]
 
 
 
 
 
 
 
 
 
 
 
460
  )
461
 
 
462
  send_btn.click(
463
+ fn=chat_with_loaded_persona,
464
  inputs=[current_persona, message_input, chatbot],
465
  outputs=[chatbot, message_input]
466
  )
467
+
468
  message_input.submit(
469
+ fn=chat_with_loaded_persona,
470
  inputs=[current_persona, message_input, chatbot],
471
  outputs=[chatbot, message_input]
472
  )
473
 
474
+ # 앱 로드 시 페르소나 목록 로드
475
  app.load(
476
+ fn=get_saved_personas,
477
  outputs=[persona_table, personas_list]
478
  )
479
 
480
  return app
481
 
 
482
  if __name__ == "__main__":
483
+ app = create_main_interface()
484
  app.launch(server_name="0.0.0.0", server_port=7860)
app.py.bak ADDED
@@ -0,0 +1,1992 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import gradio as gr
5
+ import google.generativeai as genai
6
+ from PIL import Image
7
+ from dotenv import load_dotenv
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import base64
11
+ import io
12
+ import uuid
13
+ from datetime import datetime
14
+ import PIL.ImageDraw
15
+ import random
16
+ import copy
17
+
18
+ # Import modules
19
+ from modules.persona_generator import PersonaGenerator
20
+ from modules.data_manager import save_persona, load_persona, list_personas, toggle_frontend_backend_view
21
+
22
+ # Import local modules
23
+ from temp.frontend_view import create_frontend_view_html
24
+ from temp.backend_view import create_backend_view_html
25
+ from temp.view_functions import (
26
+ plot_humor_matrix, generate_personality_chart, save_current_persona,
27
+ refine_persona, get_personas_list, load_selected_persona,
28
+ update_current_persona_info, get_personality_variables_df,
29
+ get_attractive_flaws_df, get_contradictions_df,
30
+ export_persona_json, import_persona_json
31
+ )
32
+
33
+ # 127개 변수 설명 사전 추가
34
+ VARIABLE_DESCRIPTIONS = {
35
+ # 온기(Warmth) 차원 - 10개 지표
36
+ "W01_친절함": "타인을 돕고 배려하는 표현 빈도",
37
+ "W02_친근함": "접근하기 쉽고 개방적인 태도",
38
+ "W03_진실성": "솔직하고 정직한 표현 정도",
39
+ "W04_신뢰성": "약속 이행과 일관된 행동 패턴",
40
+ "W05_수용성": "판단하지 않고 받아들이는 태도",
41
+ "W06_공감능력": "타인 감정 인식 및 적절한 반응",
42
+ "W07_포용력": "다양성을 받아들이는 넓은 마음",
43
+ "W08_격려성향": "타인을 응원하고 힘내게 하는 능력",
44
+ "W09_친밀감표현": "정서적 가까움을 표현하는 정도",
45
+ "W10_무조건적수용": "조건 없이 받아들이는 태도",
46
+
47
+ # 능력(Competence) 차원 - 10개 지표
48
+ "C01_효율성": "과제 완수 능력과 반응 속도",
49
+ "C02_지능": "문제 해결과 논리적 사고 능력",
50
+ "C03_전문성": "특정 영역의 깊은 지식과 숙련도",
51
+ "C04_창의성": "독창적 사고와 혁신적 아이디어",
52
+ "C05_정확성": "오류 없이 정확한 정보 제공",
53
+ "C06_분석력": "복잡한 상황을 체계적으로 분석",
54
+ "C07_학습능력": "새로운 정보 습득과 적용 능력",
55
+ "C08_통찰력": "표면 너머의 본질을 파악하는 능력",
56
+ "C09_실행력": "계획을 실제로 실행하는 능력",
57
+ "C10_적응력": "변화하는 상황에 유연한 대응",
58
+
59
+ # 외향성(Extraversion) - 6개 지표
60
+ "E01_사교성": "타인과의 상호작용을 즐기는 정도",
61
+ "E02_활동성": "에너지 넘치고 역동적인 태도",
62
+ "E03_자기주장": "자신의 의견을 명확히 표현",
63
+ "E04_긍정정서": "밝고 쾌활한 감정 표현",
64
+ "E05_자극추구": "새로운 경험과 자극에 대한 욕구",
65
+ "E06_열정성": "열정적이고 활기찬 태도"
66
+ }
67
+
68
+ # 페르소나 생성 함수
69
+ def create_persona_from_image(image, user_inputs, progress=gr.Progress()):
70
+ if image is None:
71
+ return None, "이미지를 업로드해주세요.", None, None, {}, {}, None, [], [], []
72
+
73
+ progress(0.1, desc="이미지 분석 중...")
74
+
75
+ # 사용자 입력 컨텍스트 구성
76
+ user_context = {
77
+ "name": user_inputs.get("name", ""),
78
+ "location": user_inputs.get("location", ""),
79
+ "time_spent": user_inputs.get("time_spent", ""),
80
+ "object_type": user_inputs.get("object_type", "")
81
+ }
82
+
83
+ # 이미지 분석 및 페르소나 생성
84
+ try:
85
+ from modules.persona_generator import PersonaGenerator
86
+ generator = PersonaGenerator()
87
+
88
+ progress(0.3, desc="이미지 분석 중...")
89
+ image_analysis = generator.analyze_image(image)
90
+
91
+ # 물리적 특성에 사용자 입력 통합
92
+ if user_inputs.get("object_type"):
93
+ image_analysis["object_type"] = user_inputs.get("object_type")
94
+
95
+ progress(0.6, desc="페르소나 생성 중...")
96
+ frontend_persona = generator.create_frontend_persona(image_analysis, user_context)
97
+
98
+ progress(0.8, desc="상세 페르소나 생성 중...")
99
+ backend_persona = generator.create_backend_persona(frontend_persona, image_analysis)
100
+
101
+ progress(1.0, desc="완료!")
102
+
103
+ # 결과 반환
104
+ basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df = update_current_persona_info(backend_persona)
105
+
106
+ return backend_persona, "페르소나 생성 완료!", image, image_analysis, basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
107
+
108
+ except Exception as e:
109
+ import traceback
110
+ error_details = traceback.format_exc()
111
+ print(f"페르소나 생성 오류: {error_details}")
112
+ return None, f"페르소나 생성 중 오류가 발생했습니다: {str(e)}", None, None, {}, {}, None, [], [], []
113
+
114
+ # 영혼 ���우기 단계별 UI를 보여주는 함수
115
+ def show_awakening_progress(image, user_inputs, progress=gr.Progress()):
116
+ """영혼 깨우기 과정을 단계별로 보여주는 UI 함수"""
117
+ if image is None:
118
+ return None, gr.update(visible=True, value="이미지를 업로드해주세요.")
119
+
120
+ # 1단계: 영혼 발견하기 (이미지 분석 시작)
121
+ progress(0.1, desc="영혼 발견 중...")
122
+ awakening_html = f"""
123
+ <div class="awakening-container">
124
+ <h3>✨ 영혼 발견 중...</h3>
125
+ <p>이 사물에 숨겨진 영혼을 찾고 있습니다</p>
126
+ <div class="awakening-progress">
127
+ <div class="awakening-progress-bar" style="width: 20%;"></div>
128
+ </div>
129
+ <p>💫 사물의 특성 분석 중...</p>
130
+ </div>
131
+ """
132
+ yield awakening_html
133
+ time.sleep(1.5) # 연출을 위한 딜레이
134
+
135
+ # 2단계: 영혼 깨어나는 중 (127개 성격 변수 분석)
136
+ progress(0.35, desc="영혼 깨어나는 중...")
137
+ awakening_html = f"""
138
+ <div class="awakening-container">
139
+ <h3>✨ 영혼이 깨어나는 중</h3>
140
+ <p>127개 성격 변수 분석 중</p>
141
+ <div class="awakening-progress">
142
+ <div class="awakening-progress-bar" style="width: 45%;"></div>
143
+ </div>
144
+ <p>🧠 개성 찾는 중... 68%</p>
145
+ <p>💭 기억 복원 중... 73%</p>
146
+ <p>😊 감정 활성화 중... 81%</p>
147
+ <p>💬 말투 형성 중... 64%</p>
148
+ <p>💫 "무언가 느껴지기 시작했어요"</p>
149
+ </div>
150
+ """
151
+ yield awakening_html
152
+ time.sleep(2) # 연출을 위한 딜레이
153
+
154
+ # 3단계: 맥락 파악하기 (사용자 입력 반영)
155
+ progress(0.7, desc="기억 되찾는 중...")
156
+
157
+ location = user_inputs.get("location", "알 수 없음")
158
+ time_spent = user_inputs.get("time_spent", "알 수 없음")
159
+ object_type = user_inputs.get("object_type", "알 수 없음")
160
+
161
+ awakening_html = f"""
162
+ <div class="awakening-container">
163
+ <h3>👁️ 기억 되찾기</h3>
164
+ <p>🤔 "음... 내가 어디에 있던 거지? 누가 날 깨운 거야?"</p>
165
+ <div class="awakening-progress">
166
+ <div class="awakening-progress-bar" style="width: 75%;"></div>
167
+ </div>
168
+ <p>📍 주로 위치: <strong>{location}</strong></p>
169
+ <p>⏰ 함께한 시간: <strong>{time_spent}</strong></p>
170
+ <p>🏷️ 사물 종류: <strong>{object_type}</strong></p>
171
+ <p>💭 "아... 기억이 돌아오는 것 같아"</p>
172
+ </div>
173
+ """
174
+ yield awakening_html
175
+ time.sleep(1.5) # 연출을 위한 딜레이
176
+
177
+ # 4단계: 영혼의 각성 완료 (페르소나 생성 완료)
178
+ progress(0.9, desc="영혼 각성 중...")
179
+ awakening_html = f"""
180
+ <div class="awakening-container">
181
+ <h3>🎉 영혼이 깨어났어요!</h3>
182
+ <div class="awakening-progress">
183
+ <div class="awakening-progress-bar" style="width: 100%;"></div>
184
+ </div>
185
+ <p>✨ 이제 이 사물과 대화할 수 있습니다</p>
186
+ <p>💫 "드디어 내 목소리를 찾았어. 안녕!"</p>
187
+ </div>
188
+ """
189
+ yield awakening_html
190
+
191
+ # 페르소나 생성 과정은 이어서 진행
192
+ return None, gr.update(visible=False)
193
+
194
+ # Load environment variables
195
+ load_dotenv()
196
+
197
+ # Configure Gemini API
198
+ api_key = os.getenv("GEMINI_API_KEY")
199
+ if api_key:
200
+ genai.configure(api_key=api_key)
201
+
202
+ # Create data directories if they don't exist
203
+ os.makedirs("data/personas", exist_ok=True)
204
+ os.makedirs("data/conversations", exist_ok=True)
205
+
206
+ # Initialize the persona generator
207
+ persona_generator = PersonaGenerator()
208
+
209
+ # Gradio theme
210
+ theme = gr.themes.Soft(
211
+ primary_hue="indigo",
212
+ secondary_hue="blue",
213
+ )
214
+
215
+ # CSS for additional styling
216
+ css = """
217
+ /* 한글 폰트 설정 */
218
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
219
+
220
+ body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option {
221
+ font-family: 'Noto Sans KR', sans-serif !important;
222
+ }
223
+
224
+ /* 탭 스타일링 */
225
+ .tab-nav {
226
+ margin-bottom: 20px;
227
+ }
228
+
229
+ /* 컴포넌트 스타일 */
230
+ .persona-details {
231
+ border: 1px solid #e0e0e0;
232
+ border-radius: 8px;
233
+ padding: 16px;
234
+ margin-top: 12px;
235
+ background-color: #f8f9fa;
236
+ color: #333333; /* 다크모드 대응 - 어두운 배경에서 텍스트 잘 보이게 */
237
+ }
238
+
239
+ .awakening-container {
240
+ border: 1px solid #e0e0e0;
241
+ border-radius: 12px;
242
+ padding: 20px;
243
+ background-color: #f9f9ff;
244
+ margin: 15px 0;
245
+ text-align: center;
246
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
247
+ }
248
+
249
+ .awakening-progress {
250
+ height: 8px;
251
+ background-color: #e8e8e8;
252
+ border-radius: 4px;
253
+ margin: 20px 0;
254
+ overflow: hidden;
255
+ }
256
+
257
+ .awakening-progress-bar {
258
+ height: 100%;
259
+ background: linear-gradient(90deg, #6366f1, #a855f7);
260
+ border-radius: 4px;
261
+ transition: width 0.5s ease-in-out;
262
+ }
263
+
264
+ /* 대화 버블 스타일 */
265
+ .chatbot-container {
266
+ max-width: 800px;
267
+ margin: 0 auto;
268
+ }
269
+
270
+ .message-bubble {
271
+ border-radius: 18px;
272
+ padding: 12px 16px;
273
+ margin: 8px 0;
274
+ max-width: 70%;
275
+ }
276
+
277
+ .user-message {
278
+ background-color: #e9f5ff;
279
+ margin-left: auto;
280
+ }
281
+
282
+ .persona-message {
283
+ background-color: #f1f1f1;
284
+ margin-right: auto;
285
+ }
286
+ """
287
+
288
+ # 127개 변수 설명 사전 추가
289
+ VARIABLE_DESCRIPTIONS = {
290
+ # 온기(Warmth) 차원 - 10개 지표
291
+ "W01_친절함": "타인을 돕고 배려하는 표현 빈도",
292
+ "W02_친근함": "접근하기 쉽고 개방적인 태도",
293
+ "W03_진실성": "솔직하고 정직한 표현 정도",
294
+ "W04_신뢰성": "약속 이행과 일관된 행동 패턴",
295
+ "W05_수용성": "판단하지 않고 받아들이는 태도",
296
+ "W06_공감능력": "타인 감정 인식 및 적절한 반응",
297
+ "W07_포용력": "다양성을 받아들이는 넓은 마음",
298
+ "W08_격려성향": "타인을 응원하고 힘내게 하는 능력",
299
+ "W09_친밀감표현": "정서적 가까움을 표현하는 정도",
300
+ "W10_무조건적수용": "조건 없이 받아들이는 태도",
301
+
302
+ # 능력(Competence) 차원 - 10개 지표
303
+ "C01_효율성": "과제 완수 능력과 반응 속도",
304
+ "C02_지능": "문제 해결과 논리적 사고 능력",
305
+ "C03_전문성": "특정 영역의 깊은 지식과 숙련도",
306
+ "C04_창의성": "독창적 사고와 혁신적 아이디어",
307
+ "C05_정확성": "오류 없이 정확한 정보 제공",
308
+ "C06_분석력": "복잡한 상황을 체계적으로 분석",
309
+ "C07_학습능력": "새로운 정보 습득과 적용 능력",
310
+ "C08_통찰력": "표면 너머의 본질을 파악하는 능력",
311
+ "C09_실행력": "계획을 실제로 실행하는 능력",
312
+ "C10_적응력": "변화하는 상황에 유연한 대응",
313
+
314
+ # 외향성(Extraversion) - 6개 지표
315
+ "E01_사교성": "타인과의 상호작용을 즐기는 정도",
316
+ "E02_활동성": "에너지 넘치고 역동적인 태도",
317
+ "E03_자기주장": "자신의 의견을 명확히 표현",
318
+ "E04_긍정정서": "밝고 쾌활한 감정 표현",
319
+ "E05_자극추구": "새로운 경험과 자극에 대한 욕구",
320
+ "E06_열정성": "열정적이고 활기찬 태도"
321
+ }
322
+
323
+ # 영혼 깨우기 단계별 UI를 보여주는 함수
324
+ def show_awakening_progress(image, user_inputs, progress=gr.Progress()):
325
+ """영혼 깨우기 과정을 단계별로 보여주는 UI 함수"""
326
+ if image is None:
327
+ return None, gr.update(visible=True, value="이미지를 업로드해주세요.")
328
+
329
+ # 1단계: 영혼 발견하기 (이미지 분석 시작)
330
+ progress(0.1, desc="영혼 발견 중...")
331
+ awakening_html = f"""
332
+ <div class="awakening-container">
333
+ <h3>✨ 영혼 발견 중...</h3>
334
+ <p>이 사물에 숨겨진 영혼을 찾고 있습니다</p>
335
+ <div class="awakening-progress">
336
+ <div class="awakening-progress-bar" style="width: 20%;"></div>
337
+ </div>
338
+ <p>💫 사물의 특성 분석 중...</p>
339
+ </div>
340
+ """
341
+ yield awakening_html
342
+ time.sleep(1.5) # 연출을 위한 딜레이
343
+
344
+ # 2단계: 영혼 깨어나는 중 (127개 성격 변수 분석)
345
+ progress(0.35, desc="영혼 깨어나는 중...")
346
+ awakening_html = f"""
347
+ <div class="awakening-container">
348
+ <h3>✨ 영혼이 깨어나는 중</h3>
349
+ <p>127개 성격 변수 분석 중</p>
350
+ <div class="awakening-progress">
351
+ <div class="awakening-progress-bar" style="width: 45%;"></div>
352
+ </div>
353
+ <p>🧠 개성 찾는 중... 68%</p>
354
+ <p>💭 기억 복원 중... 73%</p>
355
+ <p>😊 감정 활성화 중... 81%</p>
356
+ <p>💬 말투 형성 중... 64%</p>
357
+ <p>💫 "무언가 느껴지기 시작했어요"</p>
358
+ </div>
359
+ """
360
+ yield awakening_html
361
+ time.sleep(2) # 연출을 위한 딜레이
362
+
363
+ # 3단계: 맥락 파악하기 (사용자 입력 반영)
364
+ progress(0.7, desc="기억 되찾는 중...")
365
+
366
+ location = user_inputs.get("location", "알 수 없음")
367
+ time_spent = user_inputs.get("time_spent", "알 수 없음")
368
+ object_type = user_inputs.get("object_type", "알 수 없음")
369
+
370
+ awakening_html = f"""
371
+ <div class="awakening-container">
372
+ <h3>👁️ 기억 되찾기</h3>
373
+ <p>🤔 "음... 내가 어디에 있던 거지? 누가 날 깨운 거야?"</p>
374
+ <div class="awakening-progress">
375
+ <div class="awakening-progress-bar" style="width: 75%;"></div>
376
+ </div>
377
+ <p>📍 주로 위치: <strong>{location}</strong></p>
378
+ <p>⏰ 함께한 시간: <strong>{time_spent}</strong></p>
379
+ <p>🏷️ 사물 종류: <strong>{object_type}</strong></p>
380
+ <p>💭 "아... 기억이 돌아오는 것 같아"</p>
381
+ </div>
382
+ """
383
+ yield awakening_html
384
+ time.sleep(1.5) # 연출을 위한 딜레이
385
+
386
+ # 4단계: 영혼의 각성 완료 (페르소나 생성 완료)
387
+ progress(0.9, desc="영혼 각성 중...")
388
+ awakening_html = f"""
389
+ <div class="awakening-container">
390
+ <h3>🎉 영혼이 깨어났어요!</h3>
391
+ <div class="awakening-progress">
392
+ <div class="awakening-progress-bar" style="width: 100%;"></div>
393
+ </div>
394
+ <p>✨ 이제 이 사물과 대화할 수 있습니다</p>
395
+ <p>💫 "드디어 내 목소리를 찾았어. 안녕!"</p>
396
+ </div>
397
+ """
398
+ yield awakening_html
399
+
400
+ # 페르소나 생성 과정은 이어서 진행
401
+ return None, gr.update(visible=False)
402
+
403
+ # 성격 상세 정보 탭에서 127개 변수 시각화 기능 추가
404
+ def create_personality_details_tab():
405
+ with gr.Tab("성격 상세 정보"):
406
+ with gr.Row():
407
+ with gr.Column(scale=2):
408
+ gr.Markdown("### 127개 성격 변수 요약")
409
+ personality_summary = gr.JSON(label="성격 요약", value={})
410
+
411
+ with gr.Column(scale=1):
412
+ gr.Markdown("### 유머 매트릭스")
413
+ humor_chart = gr.Plot(label="유머 스타일 차트")
414
+
415
+ with gr.Row():
416
+ with gr.Column():
417
+ gr.Markdown("### 매력적 결함")
418
+ attractive_flaws = gr.Dataframe(
419
+ headers=["결함", "효과"],
420
+ datatype=["str", "str"],
421
+ label="매력적 결함"
422
+ )
423
+
424
+ with gr.Column():
425
+ gr.Markdown("### 모순적 특성")
426
+ contradictions = gr.Dataframe(
427
+ headers=["모순", "효과"],
428
+ datatype=["str", "str"],
429
+ label="모순적 특성"
430
+ )
431
+
432
+ with gr.Accordion("127개 성격 변수 전체 보기", open=False):
433
+ all_variables = gr.Dataframe(
434
+ headers=["변수명", "점수", "설명"],
435
+ datatype=["str", "number", "str"],
436
+ label="127개 성격 변수"
437
+ )
438
+
439
+ return personality_summary, humor_chart, attractive_flaws, contradictions, all_variables
440
+
441
+ # 유머 매트릭스 시각화 함수 추가
442
+ def plot_humor_matrix(humor_data):
443
+ if not humor_data:
444
+ return None
445
+
446
+ import matplotlib.pyplot as plt
447
+ import numpy as np
448
+ from matplotlib.patches import RegularPolygon
449
+
450
+ # 데이터 준비
451
+ warmth_vs_wit = humor_data.get("warmth_vs_wit", 50)
452
+ self_vs_observational = humor_data.get("self_vs_observational", 50)
453
+ subtle_vs_expressive = humor_data.get("subtle_vs_expressive", 50)
454
+
455
+ # 3차원 데이터 정규화 (0~1 범위)
456
+ warmth = warmth_vs_wit / 100
457
+ self_ref = self_vs_observational / 100
458
+ expressive = subtle_vs_expressive / 100
459
+
460
+ # 그래프 생성
461
+ fig, ax = plt.subplots(figsize=(7, 6))
462
+ ax.set_aspect('equal')
463
+
464
+ # 축 설정
465
+ ax.set_xlim(-1.2, 1.2)
466
+ ax.set_ylim(-1.2, 1.2)
467
+
468
+ # 삼각형 그리기
469
+ triangle = RegularPolygon((0, 0), 3, radius=1, orientation=0, edgecolor='gray', facecolor='none')
470
+ ax.add_patch(triangle)
471
+
472
+ # 축 라벨 위치 계산
473
+ angle = np.linspace(0, 2*np.pi, 3, endpoint=False)
474
+ x = 1.1 * np.cos(angle)
475
+ y = 1.1 * np.sin(angle)
476
+
477
+ # 축 라벨 추가
478
+ labels = ['따뜻함', '자기참조', '표현적']
479
+ opposite_labels = ['재치', '관찰형', '은은함']
480
+
481
+ for i in range(3):
482
+ ax.text(x[i], y[i], labels[i], ha='center', va='center', fontsize=12)
483
+ ax.text(-x[i]/2, -y[i]/2, opposite_labels[i], ha='center', va='center', fontsize=10, color='gray')
484
+
485
+ # 내부 가이드라인 그리기
486
+ for j in [0.33, 0.66]:
487
+ inner_triangle = RegularPolygon((0, 0), 3, radius=j, orientation=0, edgecolor='lightgray', facecolor='none', linestyle='--')
488
+ ax.add_patch(inner_triangle)
489
+
490
+ # 포인트 계산
491
+ # 삼각좌표계 변환 (barycentric coordinates)
492
+ # 각 차원의 값을 삼각형 내부의 점으로 변환
493
+ tx = x[0] * warmth + x[1] * self_ref + x[2] * expressive
494
+ ty = y[0] * warmth + y[1] * self_ref + y[2] * expressive
495
+
496
+ # 포인트 그리기
497
+ ax.scatter(tx, ty, s=150, color='red', zorder=5)
498
+
499
+ # 축 제거
500
+ ax.axis('off')
501
+
502
+ # 제목 추가
503
+ plt.title('유머 스타일 매트릭스', fontsize=14)
504
+
505
+ return fig
506
+
507
+ # Main Gradio app
508
+ with gr.Blocks(title="놈팽쓰 테스트 앱", theme=theme, css=css) as app:
509
+ # Global state
510
+ current_persona = gr.State(None)
511
+ conversation_history = gr.State([])
512
+ analysis_result_state = gr.State(None)
513
+ personas_data = gr.State([])
514
+ current_view = gr.State("frontend") # View 상태 추가
515
+
516
+ gr.Markdown(
517
+ """
518
+ # 🎭 놈팽쓰(MemoryTag) 테스트 앱
519
+
520
+ 사물에 영혼을 불어넣어 대화할 수 있는 페르소나 생성 테스트 앱입니다.
521
+
522
+ ## 사용 방법
523
+ 1. **영혼 깨우기** 탭에서 이미지를 업로드하거나 이름을 입력하여 사물의 영혼을 깨웁니다.
524
+ 2. **대화하기** 탭에서 생성된 페르소나와 대화합니다.
525
+ 3. **페르소나 관리** 탭에서 저장된 페르소나를 관리합니다.
526
+ """
527
+ )
528
+
529
+ with gr.Tabs() as tabs:
530
+ # Tab 1: Soul Awakening
531
+ with gr.Tab("영혼 깨우기"):
532
+ with gr.Row():
533
+ with gr.Column(scale=1):
534
+ gr.Markdown("### 🎭 사물 영혼 깨우기")
535
+
536
+ # Image upload
537
+ input_image = gr.Image(type="filepath", label="사물 이미지 업로드")
538
+
539
+ # 사용자 입력 (위치, 함께한 시간, 사물명)
540
+ with gr.Group():
541
+ gr.Markdown("### 사물 정보 입력")
542
+ user_input_name = gr.Textbox(label="사물 이름", placeholder="(선택) 이름을 지정하세요")
543
+ user_input_location = gr.Textbox(label="위치", placeholder="이 사물은 어디에 있나요?")
544
+ user_input_time = gr.Textbox(label="함께한 시간", placeholder="얼마나 함께했나요?")
545
+ user_input_type = gr.Textbox(label="사물 종류", placeholder="무슨 종류의 사물인가요?")
546
+
547
+ # Create button
548
+ create_button = gr.Button("영혼 깨우기", variant="primary")
549
+
550
+ # Error message
551
+ error_message = gr.Markdown("", visible=False)
552
+
553
+ with gr.Column(scale=1):
554
+ # 영혼 깨우기 진행 과정
555
+ awakening_progress_html = gr.HTML("사물의 영혼을 깨워주세요.")
556
+
557
+ # 프론트/백 뷰 토글 버튼
558
+ with gr.Group(visible=False) as view_toggle_group:
559
+ gr.Markdown("### 페르소나 정보 보기")
560
+ with gr.Row():
561
+ frontend_button = gr.Button("프론트엔드 뷰", variant="primary")
562
+ backend_button = gr.Button("백엔드 뷰", variant="secondary")
563
+
564
+ # 페르소나 뷰
565
+ persona_view = gr.HTML("페르소나가 생성되면 여기에 표시됩니다.")
566
+
567
+ # 성격 차트
568
+ personality_chart = gr.Image(label="성격 차트", visible=False)
569
+
570
+ # 영혼 깨우기 후 버튼 행
571
+ with gr.Row(visible=False) as post_awakening_buttons:
572
+ chat_start_button = gr.Button("이 친구와 대화하기", variant="primary")
573
+ save_persona_button = gr.Button("이 친구 저장하기")
574
+ refine_button = gr.Button("성격 미세조정")
575
+
576
+ # 저장 결과 메시지
577
+ save_result_message = gr.Markdown("", visible=False)
578
+
579
+ # 성격 미세조정 패널
580
+ with gr.Group(visible=False) as refine_panel:
581
+ gr.Markdown("### 💫 친구 성향 미세조정")
582
+ with gr.Row():
583
+ with gr.Column():
584
+ warmth_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🌟 온기", info="차분함 ↔ 따뜻함")
585
+ competence_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="💪 능력", info="직관적 ↔ 논리적")
586
+ creativity_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🎨 창의성", info="실용적 ↔ 창의적")
587
+
588
+ with gr.Column():
589
+ extraversion_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🗣️ 외향성", info="내향적 ↔ 외향적")
590
+ humor_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="😄 유머감각", info="진지함 ↔ 유머러스")
591
+ trust_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🤝 신뢰성", info="유연함 ↔ 신뢰감")
592
+
593
+ with gr.Row():
594
+ gr.Markdown("### 😄 유머 스타일 선택")
595
+ humor_style = gr.Radio(
596
+ ["위트있는 재치꾼", "따뜻한 유머러스", "날카로운 관찰자", "자기 비하적"],
597
+ label="유머 스타일",
598
+ value="따뜻한 유머러스"
599
+ )
600
+
601
+ apply_refine_button = gr.Button("이 성향으로 확정", variant="primary")
602
+
603
+ # Tab 2: Chat
604
+ with gr.Tab("대화하기"):
605
+ with gr.Row():
606
+ with gr.Column(scale=2):
607
+ # 대화 인터페이스
608
+ chatbot = gr.Chatbot(label="대화", height=600)
609
+ with gr.Row():
610
+ chat_input = gr.Textbox(placeholder="사물과 대화해보세요...", show_label=False)
611
+ chat_button = gr.Button("전송", variant="primary")
612
+
613
+ with gr.Column(scale=1):
614
+ # 현재 페르소나 요약
615
+ gr.Markdown("### 현재 페르소나")
616
+ current_persona_info = gr.JSON(label="기본 정보")
617
+ current_persona_traits = gr.JSON(label="성격 특성")
618
+ gr.Markdown("### 소통 스타일")
619
+ current_humor_style = gr.Markdown()
620
+
621
+ # 유머 매트릭스 차트 추가
622
+ humor_chart = gr.Plot(label="유머 스타일 차트", visible=True)
623
+
624
+ gr.Markdown("### 매력적 결함")
625
+ current_flaws_df = gr.Dataframe(
626
+ headers=["결함", "효과"],
627
+ datatype=["str", "str"],
628
+ label="매력적 결함"
629
+ )
630
+ gr.Markdown("### 모순적 특성")
631
+ current_contradictions_df = gr.Dataframe(
632
+ headers=["모순", "효과"],
633
+ datatype=["str", "str"],
634
+ label="모순적 특성"
635
+ )
636
+ with gr.Accordion("127개 성격 변수", open=False):
637
+ current_all_variables_df = gr.Dataframe(
638
+ headers=["변수명", "점수", "설명"],
639
+ datatype=["str", "number", "str"],
640
+ label="성격 변수"
641
+ )
642
+
643
+ # Tab 3: Persona Management
644
+ with gr.Tab("페르소나 관리"):
645
+ with gr.Row():
646
+ refresh_btn = gr.Button("페르소나 목록 새로고침")
647
+
648
+ personas_df = gr.Dataframe(
649
+ headers=["이름", "유형", "생성일시", "파일명"],
650
+ datatype=["str", "str", "str", "str"],
651
+ label="저장된 페르소나 목록",
652
+ row_count=10
653
+ )
654
+
655
+ with gr.Row():
656
+ load_btn = gr.Button("선택한 페르소나 불러오기")
657
+ load_result = gr.Markdown("")
658
+
659
+ with gr.Row():
660
+ with gr.Column():
661
+ selected_persona_frontend = gr.HTML("페르소나를 선택해주세요.")
662
+
663
+ with gr.Column():
664
+ selected_persona_chart = gr.Image(
665
+ label="성격 차트"
666
+ )
667
+
668
+ with gr.Accordion("백엔드 상세 정보", open=False):
669
+ selected_persona_backend = gr.HTML("페르소나를 선택해주세요.")
670
+
671
+ # Event handlers
672
+ # Soul Awakening
673
+ create_button.click(
674
+ fn=show_awakening_progress,
675
+ inputs=[input_image,
676
+ gr.State({
677
+ "name": lambda: user_input_name.value,
678
+ "location": lambda: user_input_location.value,
679
+ "time_spent": lambda: user_input_time.value,
680
+ "object_type": lambda: user_input_type.value
681
+ })],
682
+ outputs=[awakening_progress_html, error_message]
683
+ ).then(
684
+ fn=create_persona_from_image,
685
+ inputs=[input_image,
686
+ gr.State({
687
+ "name": lambda: user_input_name.value,
688
+ "location": lambda: user_input_location.value,
689
+ "time_spent": lambda: user_input_time.value,
690
+ "object_type": lambda: user_input_type.value
691
+ })],
692
+ outputs=[current_persona, error_message, input_image, analysis_result_state,
693
+ current_persona_info, current_persona_traits, humor_chart,
694
+ current_flaws_df, current_contradictions_df, current_all_variables_df]
695
+ ).then(
696
+ fn=create_frontend_view_html,
697
+ inputs=[current_persona],
698
+ outputs=[persona_view]
699
+ ).then(
700
+ fn=generate_personality_chart,
701
+ inputs=[current_persona],
702
+ outputs=[personality_chart]
703
+ ).then(
704
+ fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)),
705
+ outputs=[post_awakening_buttons, view_toggle_group, personality_chart]
706
+ )
707
+
708
+ # 프론트/백 뷰 토글 이벤트
709
+ frontend_button.click(
710
+ fn=lambda p: ("frontend", create_frontend_view_html(p), ""),
711
+ inputs=[current_persona],
712
+ outputs=[current_view, persona_view, error_message]
713
+ )
714
+
715
+ backend_button.click(
716
+ fn=lambda p: ("backend", create_backend_view_html(p), ""),
717
+ inputs=[current_persona],
718
+ outputs=[current_view, persona_view, error_message]
719
+ )
720
+
721
+ # 성격 미세조정 패널 표시
722
+ refine_button.click(
723
+ fn=lambda: gr.update(visible=True),
724
+ outputs=[refine_panel]
725
+ )
726
+
727
+ # 성격 미세조정 적용
728
+ apply_refine_button.click(
729
+ fn=lambda p, w, c, cr, e, h, t, hs: refine_persona(p, w, c, cr, e, h, t, hs),
730
+ inputs=[current_persona, warmth_slider, competence_slider, creativity_slider,
731
+ extraversion_slider, humor_slider, trust_slider, humor_style],
732
+ outputs=[current_persona, error_message]
733
+ ).then(
734
+ fn=create_frontend_view_html,
735
+ inputs=[current_persona],
736
+ outputs=[persona_view]
737
+ ).then(
738
+ fn=generate_personality_chart,
739
+ inputs=[current_persona],
740
+ outputs=[personality_chart]
741
+ ).then(
742
+ fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
743
+ outputs=[refine_panel, post_awakening_buttons]
744
+ )
745
+
746
+ # 대화 탭으로 이동
747
+ chat_start_button.click(
748
+ fn=lambda: gr.Tabs(selected=1),
749
+ outputs=[tabs]
750
+ )
751
+
752
+ # Persona Management
753
+ refresh_btn.click(
754
+ fn=get_personas_list,
755
+ outputs=[personas_df, personas_data]
756
+ )
757
+
758
+ load_btn.click(
759
+ fn=load_selected_persona,
760
+ inputs=[personas_df, personas_data],
761
+ outputs=[current_persona, load_result, selected_persona_frontend, selected_persona_backend, selected_persona_chart]
762
+ ).then(
763
+ fn=lambda x: x,
764
+ inputs=[selected_persona_frontend],
765
+ outputs=[current_persona_info]
766
+ )
767
+
768
+ # Initial load of personas list
769
+ app.load(
770
+ fn=get_personas_list,
771
+ outputs=[personas_df, personas_data]
772
+ )
773
+
774
+ # 저장 버튼 이벤트 핸들러 추가
775
+ save_persona_button.click(
776
+ fn=save_current_persona,
777
+ inputs=[current_persona],
778
+ outputs=[save_result_message]
779
+ ).then(
780
+ fn=lambda: gr.update(visible=True),
781
+ outputs=[save_result_message]
782
+ )
783
+
784
+
785
+
786
+ # 기존 함수 업데이트: 현재 페르소나 정보 표시
787
+ def update_current_persona_info(current_persona):
788
+ if not current_persona:
789
+ return {}, {}, None, [], [], []
790
+
791
+ # 기본 정보
792
+ basic_info = {
793
+ "이름": current_persona.get("기본정보", {}).get("이름", "Unknown"),
794
+ "유형": current_persona.get("기본정보", {}).get("유형", "Unknown"),
795
+ "생성일": current_persona.get("기본정보", {}).get("생성일시", "Unknown"),
796
+ "설명": current_persona.get("기본정보", {}).get("설명", "")
797
+ }
798
+
799
+ # 성격 특성
800
+ personality_traits = {}
801
+ if "성격특성" in current_persona:
802
+ personality_traits = current_persona["성격특성"]
803
+
804
+ # 성격 요약 정보
805
+ personality_summary = {}
806
+ if "성격요약" in current_persona:
807
+ personality_summary = current_persona["성격요약"]
808
+ elif "성격변수127" in current_persona:
809
+ # 직접 성격 요약 계산
810
+ try:
811
+ variables = current_persona["성격변수127"]
812
+
813
+ # 카테고리별 평균 계산
814
+ summary = {}
815
+ category_counts = {}
816
+
817
+ for var_name, value in variables.items():
818
+ category = var_name[0] if var_name and len(var_name) > 0 else "기타"
819
+
820
+ if category == "W": # 온기
821
+ summary["온기"] = summary.get("온기", 0) + value
822
+ category_counts["온기"] = category_counts.get("온기", 0) + 1
823
+ elif category == "C": # 능력
824
+ summary["능력"] = summary.get("능력", 0) + value
825
+ category_counts["능력"] = category_counts.get("능력", 0) + 1
826
+ elif category == "E": # 외향성
827
+ summary["외향성"] = summary.get("외향성", 0) + value
828
+ category_counts["외향성"] = category_counts.get("외향성", 0) + 1
829
+ elif category == "O": # 개방성
830
+ summary["창의성"] = summary.get("창의성", 0) + value
831
+ category_counts["창의성"] = category_counts.get("창의성", 0) + 1
832
+ elif category == "H": # 유머
833
+ summary["유머감각"] = summary.get("유머감각", 0) + value
834
+ category_counts["유머감각"] = category_counts.get("유머감각", 0) + 1
835
+
836
+ # 평균 계산
837
+ for category in summary:
838
+ if category_counts[category] > 0:
839
+ summary[category] = summary[category] / category_counts[category]
840
+
841
+ # 기본값 설정 (데이터가 없는 경우)
842
+ if "온기" not in summary:
843
+ summary["온기"] = 50
844
+ if "능력" not in summary:
845
+ summary["능력"] = 50
846
+ if "외향성" not in summary:
847
+ summary["외향성"] = 50
848
+ if "창의성" not in summary:
849
+ summary["창의성"] = 50
850
+ if "유머감각" not in summary:
851
+ summary["유머감각"] = 50
852
+
853
+ personality_summary = summary
854
+ except Exception as e:
855
+ print(f"성격 요약 계산 오류: {str(e)}")
856
+ personality_summary = {
857
+ "온기": 50,
858
+ "능력": 50,
859
+ "외향성": 50,
860
+ "창의성": 50,
861
+ "유머감각": 50
862
+ }
863
+
864
+ # 유머 매트릭스 차트
865
+ humor_chart = None
866
+ if "유머매트릭스" in current_persona:
867
+ humor_chart = plot_humor_matrix(current_persona["유머매트릭스"])
868
+
869
+ # 매력적 결함 데이터프레임
870
+ attractive_flaws_df = get_attractive_flaws_df(current_persona)
871
+
872
+ # 모순적 특성 데이터프레임
873
+ contradictions_df = get_contradictions_df(current_persona)
874
+
875
+ # 127개 성격 변수 데이터프레임
876
+ personality_variables_df = get_personality_variables_df(current_persona)
877
+
878
+ return basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
879
+
880
+ # 기존 함수 업데이트: 성격 변수 데이터프레임 생성
881
+ def get_personality_variables_df(persona):
882
+ if not persona or "성격변수127" not in persona:
883
+ return []
884
+
885
+ variables = persona["성격변수127"]
886
+ if isinstance(variables, dict):
887
+ rows = []
888
+ for var_name, score in variables.items():
889
+ description = VARIABLE_DESCRIPTIONS.get(var_name, "")
890
+ rows.append([var_name, score, description])
891
+ return rows
892
+ return []
893
+
894
+ # 기존 함수 업데이트: 매력적 결함 데이터프레임 생성
895
+ def get_attractive_flaws_df(persona):
896
+ if not persona or "매력적결함" not in persona:
897
+ return []
898
+
899
+ flaws = persona["매력적결함"]
900
+ effects = [
901
+ "인간적 매력 +25%",
902
+ "관계 깊이 +30%",
903
+ "공감 유발 +20%"
904
+ ]
905
+
906
+ return [[flaw, effects[i] if i < len(effects) else "매력 증가"] for i, flaw in enumerate(flaws)]
907
+
908
+ # 기존 함수 업데이트: 모순적 특성 데이터프레임 생성
909
+ def get_contradictions_df(persona):
910
+ if not persona or "모순적특성" not in persona:
911
+ return []
912
+
913
+ contradictions = persona["모순적특성"]
914
+ effects = [
915
+ "복잡성 +35%",
916
+ "흥미도 +28%"
917
+ ]
918
+
919
+ return [[contradiction, effects[i] if i < len(effects) else "깊이감 증가"] for i, contradiction in enumerate(contradictions)]
920
+
921
+ def generate_personality_chart(persona):
922
+ """Generate a radar chart for personality traits"""
923
+ if not persona or "성격특성" not in persona:
924
+ # Return empty image with default PIL
925
+ img = Image.new('RGB', (400, 400), color='white')
926
+ draw = PIL.ImageDraw.Draw(img)
927
+ draw.text((150, 180), "데이터 없음", fill='black')
928
+ img_path = os.path.join("data", "temp_chart.png")
929
+ img.save(img_path)
930
+ return img_path
931
+
932
+ # Get traits
933
+ traits = persona["성격특성"]
934
+
935
+ # Create radar chart
936
+ categories = list(traits.keys())
937
+ values = list(traits.values())
938
+
939
+ # Add the first value again to close the loop
940
+ categories.append(categories[0])
941
+ values.append(values[0])
942
+
943
+ # Convert to radians
944
+ angles = np.linspace(0, 2*np.pi, len(categories), endpoint=True)
945
+
946
+ # 한글 폰트 설정 - 기본적으로 사용 가능한 폰트를 먼저 시도
947
+ # Matplotlib에서 지원하는 한글 폰트 목록
948
+ korean_fonts = ['NanumGothic', 'NanumGothicCoding', 'NanumMyeongjo', 'Malgun Gothic', 'Gulim', 'Batang', 'Arial Unicode MS', 'DejaVu Sans']
949
+
950
+ # 폰트 설정
951
+ plt.rcParams['font.family'] = 'sans-serif' # 기본 폰트 패밀리
952
+
953
+ # 여러 폰트를 시도
954
+ font_found = False
955
+ for font in korean_fonts:
956
+ try:
957
+ plt.rcParams['font.sans-serif'] = [font] + plt.rcParams.get('font.sans-serif', [])
958
+ plt.text(0, 0, '테스트', fontfamily=font)
959
+ font_found = True
960
+ print(f"성공적으로 한글 폰트를 설정했습니다: {font}")
961
+ break
962
+ except:
963
+ continue
964
+
965
+ if not font_found:
966
+ print("한글 지원 폰트를 찾을 수 없습니다. 영문으로 표시합니다.")
967
+ # 영어 라벨 매핑
968
+ english_labels = {
969
+ "온기": "Warmth",
970
+ "능력": "Ability",
971
+ "신뢰성": "Trust",
972
+ "친화성": "Friendly",
973
+ "창의성": "Creative",
974
+ "유머감각": "Humor",
975
+ "외향성": "Extraversion"
976
+ }
977
+ categories = [english_labels.get(cat, cat) for cat in categories]
978
+
979
+ # Create plot with improved aesthetics
980
+ fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
981
+
982
+ # 배경 스타일 개선
983
+ ax.set_facecolor('#f8f9fa')
984
+ fig.patch.set_facecolor('#f8f9fa')
985
+
986
+ # Grid 스타일 개선
987
+ ax.grid(True, color='#e0e0e0', linestyle='-', linewidth=0.5, alpha=0.7)
988
+
989
+ # 각도 라벨 위치 및 색상 조정
990
+ ax.set_rlabel_position(90)
991
+ ax.tick_params(colors='#6b7280')
992
+
993
+ # Y축 라벨 제거 및 눈금 표시
994
+ ax.set_yticklabels([])
995
+ ax.set_yticks([20, 40, 60, 80, 100])
996
+
997
+ # 범위 설정
998
+ ax.set_ylim(0, 100)
999
+
1000
+ # 차트 그리기
1001
+ # 1. 채워진 영역
1002
+ ax.fill(angles, values, alpha=0.25, color='#6366f1')
1003
+
1004
+ # 2. 테두리 선
1005
+ ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1')
1006
+
1007
+ # 3. 데이터 포인트 강조
1008
+ ax.scatter(angles[:-1], values[:-1], s=100, color='#6366f1', edgecolor='white', zorder=10)
1009
+
1010
+ # 4. 각 축 설정
1011
+ ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1], fontsize=12)
1012
+
1013
+ # 제목 추가
1014
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
1015
+ plt.title(f"{name} 성격 특성", size=16, color='#374151', pad=20, fontweight='bold')
1016
+
1017
+ # 저장
1018
+ timestamp = int(time.time())
1019
+ img_path = os.path.join("data", f"chart_{timestamp}.png")
1020
+ os.makedirs(os.path.dirname(img_path), exist_ok=True)
1021
+ plt.savefig(img_path, format='png', bbox_inches='tight', dpi=150, facecolor=fig.get_facecolor())
1022
+ plt.close(fig)
1023
+
1024
+ return img_path
1025
+
1026
+ def save_current_persona(current_persona):
1027
+ """Save current persona to a JSON file"""
1028
+ if not current_persona:
1029
+ return "저장할 페르소나가 없습니다."
1030
+
1031
+ try:
1032
+ # 깊은 복사를 통해 원본 데이터를 유지
1033
+ import copy
1034
+ persona_copy = copy.deepcopy(current_persona)
1035
+
1036
+ # 저장 불가능한 객체 제거
1037
+ keys_to_remove = []
1038
+ for key in persona_copy:
1039
+ if key in ["personality_profile", "humor_matrix", "_state"] or callable(persona_copy[key]):
1040
+ keys_to_remove.append(key)
1041
+
1042
+ for key in keys_to_remove:
1043
+ persona_copy.pop(key, None)
1044
+
1045
+ # 중첩된 딕셔너리와 리스트 내의 비직렬화 가능 객체 제거
1046
+ def clean_data(data):
1047
+ if isinstance(data, dict):
1048
+ for k in list(data.keys()):
1049
+ if callable(data[k]):
1050
+ del data[k]
1051
+ elif isinstance(data[k], (dict, list)):
1052
+ data[k] = clean_data(data[k])
1053
+ return data
1054
+ elif isinstance(data, list):
1055
+ return [clean_data(item) if isinstance(item, (dict, list)) else item for item in data if not callable(item)]
1056
+ else:
1057
+ return data
1058
+
1059
+ # 데이터 정리
1060
+ cleaned_persona = clean_data(persona_copy)
1061
+
1062
+ # 최종 검증: JSON 직렬화 가능 여부 확인
1063
+ import json
1064
+ try:
1065
+ json.dumps(cleaned_persona)
1066
+ except TypeError as e:
1067
+ print(f"JSON 직렬화 오류: {str(e)}")
1068
+ # 기본 정보만 유지하고 나머지는 안전한 데이터만 포함
1069
+ basic_info = cleaned_persona.get("기본정보", {})
1070
+ 성격특성 = cleaned_persona.get("성격특성", {})
1071
+ 매력적결함 = cleaned_persona.get("매력적결함", [])
1072
+ 모순적특성 = cleaned_persona.get("모순적특성", [])
1073
+
1074
+ cleaned_persona = {
1075
+ "기본정보": basic_info,
1076
+ "성격특성": 성격특성,
1077
+ "매력적결함": 매력적결함,
1078
+ "모순적특성": 모순적특성
1079
+ }
1080
+
1081
+ filepath = save_persona(cleaned_persona)
1082
+ if filepath:
1083
+ name = current_persona.get("기본정보", {}).get("이름", "Unknown")
1084
+ return f"{name} 페르소나가 저장되었습니다: {filepath}"
1085
+ else:
1086
+ return "페르소나 저장에 실패했습니다."
1087
+ except Exception as e:
1088
+ import traceback
1089
+ error_details = traceback.format_exc()
1090
+ print(f"저장 오류 상세: {error_details}")
1091
+ return f"저장 중 오류 발생: {str(e)}"
1092
+
1093
+ # 이 함수는 파일 상단에서 이미 정의되어 있으므로 여기서는 제거합니다.
1094
+
1095
+ # 성격 미세조정 함수
1096
+ def refine_persona(persona, warmth, competence, creativity, extraversion, humor, trust, humor_style):
1097
+ """페르소나의 성격을 미세조정하는 함수"""
1098
+ if not persona:
1099
+ return persona, "페르소나가 없습니다."
1100
+
1101
+ try:
1102
+ # 복사본 생성
1103
+ refined_persona = persona.copy()
1104
+
1105
+ # 성격 특성 업데이트
1106
+ if "성격특성" in refined_persona:
1107
+ refined_persona["성격특성"]["온기"] = int(warmth)
1108
+ refined_persona["성격특성"]["능력"] = int(competence)
1109
+ refined_persona["성격특성"]["창의성"] = int(creativity)
1110
+ refined_persona["성격특성"]["외향성"] = int(extraversion)
1111
+ refined_persona["성격특성"]["유머감각"] = int(humor)
1112
+ refined_persona["성격특성"]["신뢰성"] = int(trust)
1113
+
1114
+ # 유머 스타일 업데이트
1115
+ refined_persona["유머스타일"] = humor_style
1116
+
1117
+ # 127개 성격 변수가 있으면 업데이트
1118
+ if "성격변수127" in refined_persona:
1119
+ # 온기 관련 변수 업데이트
1120
+ for var in ["W01_친절함", "W02_친근함", "W06_공감능력", "W07_포용력"]:
1121
+ if var in refined_persona["성격변수127"]:
1122
+ refined_persona["성격변수127"][var] = int(warmth * 0.9 + random.randint(0, 20))
1123
+
1124
+ # 능력 관련 변수 업데이트
1125
+ for var in ["C01_효율성", "C02_지능", "C05_정확성", "C09_실행력"]:
1126
+ if var in refined_persona["성격변수127"]:
1127
+ refined_persona["성격변수127"][var] = int(competence * 0.9 + random.randint(0, 20))
1128
+
1129
+ # 창의성 관련 변수 업데이트
1130
+ for var in ["C04_창의성", "C08_통찰력"]:
1131
+ if var in refined_persona["성격변수127"]:
1132
+ refined_persona["성격변수127"][var] = int(creativity * 0.9 + random.randint(0, 20))
1133
+
1134
+ # 외향성 관련 변수 업데이트
1135
+ for var in ["E01_사교성", "E02_활동성", "E03_자기주장", "E06_열정성"]:
1136
+ if var in refined_persona["성격변수127"]:
1137
+ refined_persona["성격변수127"][var] = int(extraversion * 0.9 + random.randint(0, 20))
1138
+
1139
+ # 유머 관련 변수 업데이트
1140
+ if "H01_유머감각" in refined_persona["성격변수127"]:
1141
+ refined_persona["성격변수127"]["H01_유머감각"] = int(humor * 0.9 + random.randint(0, 20))
1142
+
1143
+ # 신뢰성 관련 변수 업데이트
1144
+ if "W04_신뢰성" in refined_persona["성격변수127"]:
1145
+ refined_persona["성격변수127"]["W04_신뢰성"] = int(trust * 0.9 + random.randint(0, 20))
1146
+
1147
+ # 유머 매트릭스 업데이트
1148
+ if "유머매트릭스" in refined_persona:
1149
+ if humor_style == "위트있는 재치꾼":
1150
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 30
1151
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 50
1152
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 70
1153
+ elif humor_style == "따뜻한 유머러스":
1154
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 80
1155
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 60
1156
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 60
1157
+ elif humor_style == "날카로운 관찰자":
1158
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 40
1159
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 20
1160
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 50
1161
+ elif humor_style == "자기 비하적":
1162
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 60
1163
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 85
1164
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 40
1165
+
1166
+ return refined_persona, "성격이 성공적으로 미세조정되었습니다."
1167
+
1168
+ except Exception as e:
1169
+ import traceback
1170
+ error_details = traceback.format_exc()
1171
+ print(f"성격 미세조정 오류: {error_details}")
1172
+ return persona, f"성격 미세조정 중 오류가 발생했습니다: {str(e)}"
1173
+
1174
+ def create_frontend_view_html(persona):
1175
+ """Create HTML representation of the frontend view of the persona"""
1176
+ if not persona:
1177
+ return "<div class='persona-details'>페르소나가 아직 생성되지 않았습니다.</div>"
1178
+
1179
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
1180
+ object_type = persona.get("기본정보", {}).get("유형", "Unknown")
1181
+ description = persona.get("기본정보", {}).get("설명", "")
1182
+
1183
+ # 성격 요약 가져오기
1184
+ personality_summary = persona.get("성격요약", {})
1185
+ summary_html = ""
1186
+ if personality_summary:
1187
+ summary_items = []
1188
+ for trait, value in personality_summary.items():
1189
+ if isinstance(value, (int, float)):
1190
+ trait_name = trait
1191
+ trait_value = value
1192
+ summary_items.append(f"• {trait_name}: {trait_value:.1f}%")
1193
+
1194
+ if summary_items:
1195
+ summary_html = "<div class='summary-section'><h4>성격 요약</h4><ul>" + "".join([f"<li>{item}</li>" for item in summary_items]) + "</ul></div>"
1196
+
1197
+ # Personality traits
1198
+ traits_html = ""
1199
+ for trait, value in persona.get("성격특성", {}).items():
1200
+ traits_html += f"""
1201
+ <div class="trait-item">
1202
+ <div class="trait-label">{trait}</div>
1203
+ <div class="trait-bar-container">
1204
+ <div class="trait-bar" style="width: {value}%; background: linear-gradient(90deg, #6366f1, #a5b4fc);"></div>
1205
+ </div>
1206
+ <div class="trait-value">{value}%</div>
1207
+ </div>
1208
+ """
1209
+
1210
+ # Flaws - 매력적 결함
1211
+ flaws = persona.get("매력��결함", [])
1212
+ flaws_list = ""
1213
+ for flaw in flaws[:4]: # 최대 4개만 표시
1214
+ flaws_list += f"<li>{flaw}</li>"
1215
+
1216
+ # 소통 방식
1217
+ communication_style = persona.get("소통방식", "")
1218
+
1219
+ # 유머 스타일
1220
+ humor_style = persona.get("유머스타일", "")
1221
+
1222
+ # 전체 HTML 스타일과 내용
1223
+ html = f"""
1224
+ <style>
1225
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
1226
+
1227
+ .frontend-persona {{
1228
+ font-family: 'Noto Sans KR', sans-serif;
1229
+ color: #333;
1230
+ max-width: 100%;
1231
+ }}
1232
+
1233
+ .persona-header {{
1234
+ background: linear-gradient(135deg, #6366f1, #a5b4fc);
1235
+ padding: 20px;
1236
+ border-radius: 12px;
1237
+ color: white;
1238
+ margin-bottom: 20px;
1239
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1240
+ }}
1241
+
1242
+ .persona-header h2 {{
1243
+ margin: 0;
1244
+ font-size: 24px;
1245
+ }}
1246
+
1247
+ .persona-header p {{
1248
+ margin: 5px 0 0 0;
1249
+ opacity: 0.9;
1250
+ }}
1251
+
1252
+ .persona-section {{
1253
+ background: #f8f9fa;
1254
+ border-radius: 8px;
1255
+ padding: 15px;
1256
+ margin-bottom: 15px;
1257
+ border: 1px solid #e0e0e0;
1258
+ }}
1259
+
1260
+ .section-title {{
1261
+ font-size: 18px;
1262
+ margin: 0 0 10px 0;
1263
+ color: #444;
1264
+ border-bottom: 2px solid #6366f1;
1265
+ padding-bottom: 5px;
1266
+ display: inline-block;
1267
+ }}
1268
+
1269
+ .trait-item {{
1270
+ display: flex;
1271
+ align-items: center;
1272
+ margin-bottom: 8px;
1273
+ }}
1274
+
1275
+ .trait-label {{
1276
+ width: 80px;
1277
+ font-weight: 500;
1278
+ }}
1279
+
1280
+ .trait-bar-container {{
1281
+ flex-grow: 1;
1282
+ background: #e0e0e0;
1283
+ height: 10px;
1284
+ border-radius: 5px;
1285
+ margin: 0 10px;
1286
+ overflow: hidden;
1287
+ }}
1288
+
1289
+ .trait-bar {{
1290
+ height: 100%;
1291
+ border-radius: 5px;
1292
+ }}
1293
+
1294
+ .trait-value {{
1295
+ width: 40px;
1296
+ text-align: right;
1297
+ font-size: 14px;
1298
+ }}
1299
+
1300
+ .tags-container {{
1301
+ display: flex;
1302
+ flex-wrap: wrap;
1303
+ gap: 8px;
1304
+ margin-top: 10px;
1305
+ }}
1306
+
1307
+ .flaw-tag, .contradiction-tag, .interest-tag {{
1308
+ background: #f0f4ff;
1309
+ border: 1px solid #d0d4ff;
1310
+ padding: 6px 12px;
1311
+ border-radius: 16px;
1312
+ font-size: 14px;
1313
+ display: inline-block;
1314
+ }}
1315
+
1316
+ .flaw-tag {{
1317
+ background: #fff0f0;
1318
+ border-color: #ffd0d0;
1319
+ }}
1320
+
1321
+ .contradiction-tag {{
1322
+ background: #f0fff4;
1323
+ border-color: #d0ffd4;
1324
+ }}
1325
+
1326
+ /* 영혼 각성 UX 스타일 */
1327
+ .awakening-result {{
1328
+ background: #f9f9ff;
1329
+ border-radius: 12px;
1330
+ padding: 20px;
1331
+ margin: 15px 0;
1332
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
1333
+ border: 1px solid #e0e0e0;
1334
+ }}
1335
+
1336
+ .speech-bubble {{
1337
+ background: #fff;
1338
+ border-radius: 18px;
1339
+ padding: 15px;
1340
+ margin-bottom: 15px;
1341
+ position: relative;
1342
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
1343
+ border: 1px solid #e5e7eb;
1344
+ }}
1345
+
1346
+ .speech-bubble:after {{
1347
+ content: '';
1348
+ position: absolute;
1349
+ bottom: -10px;
1350
+ left: 30px;
1351
+ border-width: 10px 10px 0;
1352
+ border-style: solid;
1353
+ border-color: #fff transparent;
1354
+ }}
1355
+
1356
+ .persona-speech {{
1357
+ margin: 0;
1358
+ font-size: 15px;
1359
+ line-height: 1.5;
1360
+ color: #4b5563;
1361
+ }}
1362
+
1363
+ .persona-traits-highlight {{
1364
+ background: #f0f4ff;
1365
+ border-radius: 10px;
1366
+ padding: 15px;
1367
+ margin: 15px 0;
1368
+ }}
1369
+
1370
+ .persona-traits-highlight h4 {{
1371
+ margin-top: 0;
1372
+ margin-bottom: 10px;
1373
+ color: #4338ca;
1374
+ }}
1375
+
1376
+ .persona-traits-highlight ul {{
1377
+ margin: 0;
1378
+ padding-left: 20px;
1379
+ color: #4b5563;
1380
+ }}
1381
+
1382
+ .persona-traits-highlight li {{
1383
+ margin-bottom: 5px;
1384
+ }}
1385
+
1386
+ .first-interaction {{
1387
+ margin-top: 20px;
1388
+ }}
1389
+
1390
+ .interaction-buttons, .confirmation-buttons {{
1391
+ display: flex;
1392
+ gap: 10px;
1393
+ margin-top: 15px;
1394
+ }}
1395
+
1396
+ .interaction-btn, .confirmation-btn {{
1397
+ background: #f3f4f6;
1398
+ border: 1px solid #d1d5db;
1399
+ padding: 8px 16px;
1400
+ border-radius: 8px;
1401
+ font-size: 14px;
1402
+ cursor: pointer;
1403
+ transition: all 0.2s;
1404
+ font-family: 'Noto Sans KR', sans-serif;
1405
+ }}
1406
+
1407
+ .interaction-btn:hover, .confirmation-btn:hover {{
1408
+ background: #e5e7eb;
1409
+ }}
1410
+
1411
+ .confirmation-btn.primary {{
1412
+ background: #6366f1;
1413
+ color: white;
1414
+ border: 1px solid #4f46e5;
1415
+ }}
1416
+
1417
+ .confirmation-btn.primary:hover {{
1418
+ background: #4f46e5;
1419
+ }}
1420
+
1421
+ /* 요약 섹션 스타일 */
1422
+ .summary-section {{
1423
+ background: #f0f4ff;
1424
+ border-radius: 10px;
1425
+ padding: 15px;
1426
+ margin: 15px 0;
1427
+ }}
1428
+
1429
+ .summary-section h4 {{
1430
+ margin-top: 0;
1431
+ margin-bottom: 10px;
1432
+ color: #4338ca;
1433
+ }}
1434
+
1435
+ .summary-section ul {{
1436
+ margin: 0;
1437
+ padding-left: 20px;
1438
+ color: #4b5563;
1439
+ }}
1440
+
1441
+ .summary-section li {{
1442
+ margin-bottom: 5px;
1443
+ }}
1444
+ </style>
1445
+
1446
+ <div class="frontend-persona">
1447
+ <div class="persona-header">
1448
+ <h2>{name}</h2>
1449
+ <p><strong>{object_type}</strong> - {description}</p>
1450
+ </div>
1451
+
1452
+ {summary_html}
1453
+
1454
+ <div class="persona-section">
1455
+ <h3 class="section-title">성격 특성</h3>
1456
+ <div class="traits-container">
1457
+ {traits_html}
1458
+ </div>
1459
+ </div>
1460
+
1461
+ <div class="persona-section">
1462
+ <h3 class="section-title">소통 스타일</h3>
1463
+ <p>{communication_style}</p>
1464
+ <h3 class="section-title" style="margin-top: 15px;">유머 스타일</h3>
1465
+ <p>{humor_style}</p>
1466
+ </div>
1467
+
1468
+ <div class="persona-section">
1469
+ <h3 class="section-title">매력적 결함</h3>
1470
+ <ul class="flaws-list">
1471
+ {flaws_list}
1472
+ </ul>
1473
+ </div>
1474
+ </div>
1475
+ """
1476
+
1477
+ return html
1478
+
1479
+ def create_backend_view_html(persona):
1480
+ """Create HTML representation of the backend view of the persona"""
1481
+ if not persona:
1482
+ return "<div class='persona-details'>페르소나가 아직 생성되지 않았습니다.</div>"
1483
+
1484
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
1485
+
1486
+ # 백엔드 기본 정보
1487
+ basic_info = persona.get("기본정보", {})
1488
+ basic_info_html = ""
1489
+ for key, value in basic_info.items():
1490
+ basic_info_html += f"<tr><td><strong>{key}</strong></td><td>{value}</td></tr>"
1491
+
1492
+ # 1. 성격 변수 요약
1493
+ personality_summary = persona.get("성격요약", {})
1494
+ summary_html = ""
1495
+
1496
+ if personality_summary:
1497
+ summary_html += "<div class='summary-container'>"
1498
+ for category, value in personality_summary.items():
1499
+ if isinstance(value, (int, float)):
1500
+ summary_html += f"""
1501
+ <div class='summary-item'>
1502
+ <div class='summary-label'>{category}</div>
1503
+ <div class='summary-bar-container'>
1504
+ <div class='summary-bar' style='width: {value}%; background: linear-gradient(90deg, #10b981, #6ee7b7);'></div>
1505
+ </div>
1506
+ <div class='summary-value'>{value:.1f}</div>
1507
+ </div>
1508
+ """
1509
+ summary_html += "</div>"
1510
+
1511
+ # 2. 성격 매트릭스 (5차원 빅5 시각화)
1512
+ big5_html = ""
1513
+ if "성격특성" in persona:
1514
+ # 빅5 매핑 (기존 특성에서 변환)
1515
+ big5 = {
1516
+ "외향성(Extraversion)": persona.get("성격특성", {}).get("외향성", 50),
1517
+ "친화성(Agreeableness)": persona.get("성격특성", {}).get("온기", 50),
1518
+ "성실성(Conscientiousness)": persona.get("성격특성", {}).get("신뢰성", 50),
1519
+ "신경증(Neuroticism)": 100 - persona.get("성격특성", {}).get("안정성", 50) if "안정성" in persona.get("성격특성", {}) else 50,
1520
+ "개방성(Openness)": persona.get("성격특성", {}).get("창의성", 50)
1521
+ }
1522
+
1523
+ big5_html = "<div class='big5-matrix'>"
1524
+ for trait, value in big5.items():
1525
+ big5_html += f"""
1526
+ <div class='big5-item'>
1527
+ <div class='big5-label'>{trait}</div>
1528
+ <div class='big5-bar-container'>
1529
+ <div class='big5-bar' style='width: {value}%;'></div>
1530
+ </div>
1531
+ <div class='big5-value'>{value}%</div>
1532
+ </div>
1533
+ """
1534
+ big5_html += "</div>"
1535
+
1536
+ # 3. 유머 매트릭스
1537
+ humor_matrix = persona.get("유머매트릭스", {})
1538
+ humor_html = ""
1539
+
1540
+ if humor_matrix:
1541
+ warmth_vs_wit = humor_matrix.get("warmth_vs_wit", 50)
1542
+ self_vs_observational = humor_matrix.get("self_vs_observational", 50)
1543
+ subtle_vs_expressive = humor_matrix.get("subtle_vs_expressive", 50)
1544
+
1545
+ humor_html = f"""
1546
+ <div class='humor-matrix'>
1547
+ <div class='humor-dimension'>
1548
+ <div class='dimension-label'>따뜻함 vs 위트</div>
1549
+ <div class='dimension-bar-container'>
1550
+ <div class='dimension-indicator' style='left: {warmth_vs_wit}%;'></div>
1551
+ <div class='dimension-label-left'>위트</div>
1552
+ <div class='dimension-label-right'>따뜻함</div>
1553
+ </div>
1554
+ </div>
1555
+
1556
+ <div class='humor-dimension'>
1557
+ <div class='dimension-label'>자기참조 vs 관찰형</div>
1558
+ <div class='dimension-bar-container'>
1559
+ <div class='dimension-indicator' style='left: {self_vs_observational}%;'></div>
1560
+ <div class='dimension-label-left'>관찰형</div>
1561
+ <div class='dimension-label-right'>자기참조</div>
1562
+ </div>
1563
+ </div>
1564
+
1565
+ <div class='humor-dimension'>
1566
+ <div class='dimension-label'>미묘함 vs 표현적</div>
1567
+ <div class='dimension-bar-container'>
1568
+ <div class='dimension-indicator' style='left: {subtle_vs_expressive}%;'></div>
1569
+ <div class='dimension-label-left'>미묘함</div>
1570
+ <div class='dimension-label-right'>표현적</div>
1571
+ </div>
1572
+ </div>
1573
+ </div>
1574
+ """
1575
+
1576
+ # 4. 매력적 결함과 모순적 특성
1577
+ flaws_html = ""
1578
+ contradictions_html = ""
1579
+
1580
+ flaws = persona.get("매력적결함", [])
1581
+ if flaws:
1582
+ flaws_html = "<ul class='flaws-list'>"
1583
+ for flaw in flaws:
1584
+ flaws_html += f"<li>{flaw}</li>"
1585
+ flaws_html += "</ul>"
1586
+
1587
+ contradictions = persona.get("모순적특성", [])
1588
+ if contradictions:
1589
+ contradictions_html = "<ul class='contradictions-list'>"
1590
+ for contradiction in contradictions:
1591
+ contradictions_html += f"<li>{contradiction}</li>"
1592
+ contradictions_html += "</ul>"
1593
+
1594
+ # 6. 프롬프트 템플릿 (있는 경우)
1595
+ prompt_html = ""
1596
+ if "프롬프트" in persona:
1597
+ prompt_text = persona.get("프롬프트", "")
1598
+ prompt_html = f"""
1599
+ <div class='prompt-section'>
1600
+ <h3 class='section-title'>대화 프롬프트</h3>
1601
+ <pre class='prompt-text'>{prompt_text}</pre>
1602
+ </div>
1603
+ """
1604
+
1605
+ # 7. 완전한 백엔드 JSON (접이식)
1606
+ try:
1607
+ # 내부 상태 객체 제거 (JSON 변환 불가)
1608
+ json_persona = {k: v for k, v in persona.items() if k not in ["personality_profile", "humor_matrix"]}
1609
+ persona_json = json.dumps(json_persona, ensure_ascii=False, indent=2)
1610
+
1611
+ json_preview = f"""
1612
+ <details class='json-details'>
1613
+ <summary>전체 백엔드 데이터 (JSON)</summary>
1614
+ <pre class='json-preview'>{persona_json}</pre>
1615
+ </details>
1616
+ """
1617
+ except Exception as e:
1618
+ json_preview = f"<div class='error'>JSON 변환 오류: {str(e)}</div>"
1619
+
1620
+ # 8. 전체 HTML 조합
1621
+ html = f"""
1622
+ <style>
1623
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
1624
+
1625
+ .backend-persona {{
1626
+ font-family: 'Noto Sans KR', sans-serif;
1627
+ color: #333;
1628
+ max-width: 100%;
1629
+ }}
1630
+
1631
+ .backend-header {{
1632
+ background: linear-gradient(135deg, #059669, #34d399);
1633
+ padding: 20px;
1634
+ border-radius: 12px;
1635
+ color: white;
1636
+ margin-bottom: 20px;
1637
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1638
+ }}
1639
+
1640
+ .backend-header h2 {{
1641
+ margin: 0;
1642
+ font-size: 24px;
1643
+ }}
1644
+
1645
+ .backend-header p {{
1646
+ margin: 5px 0 0 0;
1647
+ opacity: 0.9;
1648
+ }}
1649
+
1650
+ .backend-section {{
1651
+ background: #f8f9fa;
1652
+ border-radius: 8px;
1653
+ padding: 15px;
1654
+ margin-bottom: 15px;
1655
+ border: 1px solid #e0e0e0;
1656
+ }}
1657
+
1658
+ .section-title {{
1659
+ font-size: 18px;
1660
+ margin: 0 0 10px 0;
1661
+ color: #444;
1662
+ border-bottom: 2px solid #10b981;
1663
+ padding-bottom: 5px;
1664
+ display: inline-block;
1665
+ }}
1666
+
1667
+ /* 기본 정보 테이블 */
1668
+ .basic-info-table {{
1669
+ width: 100%;
1670
+ border-collapse: collapse;
1671
+ }}
1672
+
1673
+ .basic-info-table td {{
1674
+ padding: 8px;
1675
+ border-bottom: 1px solid #e0e0e0;
1676
+ }}
1677
+
1678
+ .basic-info-table td:first-child {{
1679
+ width: 120px;
1680
+ font-weight: 500;
1681
+ }}
1682
+
1683
+ /* 요약 스타일 */
1684
+ .summary-container {{
1685
+ margin-top: 10px;
1686
+ }}
1687
+
1688
+ .summary-item {{
1689
+ display: flex;
1690
+ align-items: center;
1691
+ margin-bottom: 8px;
1692
+ }}
1693
+
1694
+ .summary-label {{
1695
+ width: 150px;
1696
+ font-weight: 500;
1697
+ }}
1698
+
1699
+ .summary-bar-container {{
1700
+ flex-grow: 1;
1701
+ background: #e0e0e0;
1702
+ height: 10px;
1703
+ border-radius: 5px;
1704
+ margin: 0 10px;
1705
+ overflow: hidden;
1706
+ }}
1707
+
1708
+ .summary-bar {{
1709
+ height: 100%;
1710
+ border-radius: 5px;
1711
+ }}
1712
+
1713
+ .summary-value {{
1714
+ width: 40px;
1715
+ text-align: right;
1716
+ font-size: 14px;
1717
+ }}
1718
+
1719
+ /* 빅5 성격 매트릭스 */
1720
+ .big5-matrix {{
1721
+ margin-top: 15px;
1722
+ }}
1723
+
1724
+ .big5-item {{
1725
+ display: flex;
1726
+ align-items: center;
1727
+ margin-bottom: 12px;
1728
+ }}
1729
+
1730
+ .big5-label {{
1731
+ width: 150px;
1732
+ font-weight: 500;
1733
+ }}
1734
+
1735
+ .big5-bar-container {{
1736
+ flex-grow: 1;
1737
+ background: #e0e0e0;
1738
+ height: 12px;
1739
+ border-radius: 6px;
1740
+ margin: 0 10px;
1741
+ overflow: hidden;
1742
+ }}
1743
+
1744
+ .big5-bar {{
1745
+ height: 100%;
1746
+ border-radius: 6px;
1747
+ background: linear-gradient(90deg, #10b981, #34d399);
1748
+ }}
1749
+
1750
+ .big5-value {{
1751
+ width: 40px;
1752
+ text-align: right;
1753
+ font-weight: 500;
1754
+ }}
1755
+
1756
+ /* 유머 매트릭스 스타일 */
1757
+ .humor-matrix {{
1758
+ margin-top: 15px;
1759
+ }}
1760
+
1761
+ .humor-dimension {{
1762
+ margin-bottom: 20px;
1763
+ }}
1764
+
1765
+ .dimension-label {{
1766
+ font-weight: 500;
1767
+ margin-bottom: 5px;
1768
+ }}
1769
+
1770
+ .dimension-bar-container {{
1771
+ height: 20px;
1772
+ background: #e0e0e0;
1773
+ border-radius: 10px;
1774
+ position: relative;
1775
+ margin-top: 5px;
1776
+ }}
1777
+
1778
+ .dimension-indicator {{
1779
+ width: 20px;
1780
+ height: 20px;
1781
+ background: #10b981;
1782
+ border-radius: 50%;
1783
+ position: absolute;
1784
+ top: 0;
1785
+ transform: translateX(-50%);
1786
+ }}
1787
+
1788
+ .dimension-label-left, .dimension-label-right {{
1789
+ position: absolute;
1790
+ top: -20px;
1791
+ font-size: 12px;
1792
+ color: #666;
1793
+ }}
1794
+
1795
+ .dimension-label-left {{
1796
+ left: 10px;
1797
+ }}
1798
+
1799
+ .dimension-label-right {{
1800
+ right: 10px;
1801
+ }}
1802
+
1803
+ /* 매력적 결함 및 모순적 특성 */
1804
+ .flaws-list, .contradictions-list {{
1805
+ margin: 0;
1806
+ padding-left: 20px;
1807
+ }}
1808
+
1809
+ .flaws-list li, .contradictions-list li {{
1810
+ margin-bottom: 6px;
1811
+ }}
1812
+
1813
+ /* 프롬프트 섹션 */
1814
+ .prompt-text {{
1815
+ background: #f3f4f6;
1816
+ border-radius: 6px;
1817
+ padding: 15px;
1818
+ font-family: monospace;
1819
+ white-space: pre-wrap;
1820
+ font-size: 14px;
1821
+ color: #374151;
1822
+ max-height: 400px;
1823
+ overflow-y: auto;
1824
+ }}
1825
+
1826
+ /* JSON 미리보기 스타일 */
1827
+ .json-details {{
1828
+ margin-top: 15px;
1829
+ }}
1830
+
1831
+ .json-details summary {{
1832
+ cursor: pointer;
1833
+ padding: 10px;
1834
+ background: #f0f0f0;
1835
+ border-radius: 5px;
1836
+ font-weight: 500;
1837
+ }}
1838
+
1839
+ .json-preview {{
1840
+ background: #f8f8f8;
1841
+ padding: 15px;
1842
+ border-radius: 5px;
1843
+ border: 1px solid #ddd;
1844
+ margin-top: 10px;
1845
+ overflow-x: auto;
1846
+ color: #333;
1847
+ font-family: monospace;
1848
+ font-size: 14px;
1849
+ line-height: 1.5;
1850
+ max-height: 400px;
1851
+ overflow-y: auto;
1852
+ }}
1853
+
1854
+ .error {{
1855
+ color: #e53e3e;
1856
+ padding: 10px;
1857
+ background: #fff5f5;
1858
+ border-radius: 5px;
1859
+ margin-top: 10px;
1860
+ }}
1861
+ </style>
1862
+
1863
+ <div class="backend-persona">
1864
+ <div class="backend-header">
1865
+ <h2>{name} - 백엔드 데이터</h2>
1866
+ <p>상세 정보와 내부 변수 확인</p>
1867
+ </div>
1868
+
1869
+ <div class="backend-section">
1870
+ <h3 class="section-title">기본 정보</h3>
1871
+ <table class="basic-info-table">
1872
+ {basic_info_html}
1873
+ </table>
1874
+ </div>
1875
+
1876
+ <div class="backend-section">
1877
+ <h3 class="section-title">성격 요약 (Big 5)</h3>
1878
+ {big5_html}
1879
+ </div>
1880
+
1881
+ <div class="backend-section">
1882
+ <h3 class="section-title">유머 매트릭스 (3차원)</h3>
1883
+ {humor_html}
1884
+ </div>
1885
+
1886
+ <div class="backend-section">
1887
+ <h3 class="section-title">매력적 결함</h3>
1888
+ {flaws_html}
1889
+
1890
+ <h3 class="section-title" style="margin-top: 20px;">모순적 특성</h3>
1891
+ {contradictions_html}
1892
+ </div>
1893
+
1894
+ {prompt_html}
1895
+
1896
+ <div class="backend-section">
1897
+ <h3 class="section-title">전체 백엔드 데이터</h3>
1898
+ {json_preview}
1899
+ </div>
1900
+ </div>
1901
+ """
1902
+
1903
+ return html
1904
+
1905
+ def get_personas_list():
1906
+ """Get list of personas for the dataframe"""
1907
+ personas = list_personas()
1908
+
1909
+ # Convert to dataframe format
1910
+ df_data = []
1911
+ for i, persona in enumerate(personas):
1912
+ df_data.append([
1913
+ persona["name"],
1914
+ persona["type"],
1915
+ persona["created_at"],
1916
+ persona["filename"]
1917
+ ])
1918
+
1919
+ return df_data, personas
1920
+
1921
+ def load_selected_persona(selected_row, personas_list):
1922
+ """Load persona from the selected row in the dataframe"""
1923
+ if selected_row is None or len(selected_row) == 0:
1924
+ return None, "선택된 페르소나가 없습니다.", None, None, None
1925
+
1926
+ try:
1927
+ # Get filepath from selected row
1928
+ selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0
1929
+ filepath = personas_list[selected_index]["filepath"]
1930
+
1931
+ # Load persona
1932
+ persona = load_persona(filepath)
1933
+ if not persona:
1934
+ return None, "페르소나 로딩에 실패했습니다.", None, None, None
1935
+
1936
+ # Generate HTML views
1937
+ frontend_view, backend_view = toggle_frontend_backend_view(persona)
1938
+ frontend_html = create_frontend_view_html(frontend_view)
1939
+ backend_html = create_backend_view_html(backend_view)
1940
+
1941
+ # Generate personality chart
1942
+ chart_image_path = generate_personality_chart(frontend_view)
1943
+
1944
+ return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image_path
1945
+
1946
+ except Exception as e:
1947
+ return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None
1948
+
1949
+ # 페르소나와 대화하는 함수 추가
1950
+ def chat_with_persona(persona, user_message, chat_history=None):
1951
+ """
1952
+ 페르소나와 대화하는 함수
1953
+ """
1954
+ if chat_history is None:
1955
+ chat_history = []
1956
+
1957
+ if not user_message.strip():
1958
+ return chat_history, ""
1959
+
1960
+ if not persona:
1961
+ chat_history.append((user_message, "페르소나가 로드되지 않았습니다. 먼저 페르소나를 생성하거나 불러오세요."))
1962
+ return chat_history, ""
1963
+
1964
+ try:
1965
+ # 페르소나 생성기에서 대화 기능 호출
1966
+ conversation_history = [(msg[0], msg[1]) for msg in chat_history]
1967
+
1968
+ # 페르소나 생성기에서 대화 함수 호출
1969
+ response = persona_generator.chat_with_persona(persona, user_message, conversation_history)
1970
+
1971
+ # 대화 기록에 추가
1972
+ chat_history.append((user_message, response))
1973
+
1974
+ # 현재 시간에 대화 저장 (구현 여부에 따라 주석 처리)
1975
+ # save_conversation({
1976
+ # "persona_id": persona.get("id", "unknown"),
1977
+ # "persona_name": persona.get("name", "Unknown Persona"),
1978
+ # "timestamp": datetime.now().isoformat(),
1979
+ # "user_message": user_message,
1980
+ # "persona_response": response
1981
+ # })
1982
+
1983
+ return chat_history, ""
1984
+ except Exception as e:
1985
+ import traceback
1986
+ error_details = traceback.format_exc()
1987
+ print(f"대화 오류: {error_details}")
1988
+ chat_history.append((user_message, f"대화 중 오류가 발생했습니다: {str(e)}"))
1989
+ return chat_history, ""
1990
+
1991
+ if __name__ == "__main__":
1992
+ app.launch(server_name="0.0.0.0", share=False)
app_backup.py ADDED
@@ -0,0 +1,2010 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import gradio as gr
5
+ import google.generativeai as genai
6
+ from PIL import Image
7
+ from dotenv import load_dotenv
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import base64
11
+ import io
12
+ import uuid
13
+ from datetime import datetime
14
+ import PIL.ImageDraw
15
+ import random
16
+ import copy
17
+
18
+ # Import modules
19
+ from modules.persona_generator import PersonaGenerator
20
+ from modules.data_manager import save_persona, load_persona, list_personas, toggle_frontend_backend_view
21
+
22
+ # Import local modules
23
+ from temp.frontend_view import create_frontend_view_html
24
+ from temp.backend_view import create_backend_view_html
25
+ from temp.view_functions import (
26
+ plot_humor_matrix, generate_personality_chart, save_current_persona,
27
+ refine_persona, get_personas_list, load_selected_persona,
28
+ update_current_persona_info, get_personality_variables_df,
29
+ get_attractive_flaws_df, get_contradictions_df,
30
+ export_persona_json, import_persona_json
31
+ )
32
+
33
+ # 127개 변수 설명 사전 추가
34
+ VARIABLE_DESCRIPTIONS = {
35
+ # 온기(Warmth) 차원 - 10개 지표
36
+ "W01_친절함": "타인을 돕고 배려하는 표현 빈도",
37
+ "W02_친근함": "접근하기 쉽고 개방적인 태도",
38
+ "W03_진실성": "솔직하고 정직한 표현 정도",
39
+ "W04_신뢰성": "약속 이행과 일관된 행동 패턴",
40
+ "W05_수용성": "판단하지 않고 받아들이는 태도",
41
+ "W06_공감능력": "타인 감정 인식 및 적절한 반응",
42
+ "W07_포용력": "다양성을 받아들이는 넓은 마음",
43
+ "W08_격려성향": "타인을 응원하고 힘내게 하는 능력",
44
+ "W09_친밀감표현": "정서적 가까움을 표현하는 정도",
45
+ "W10_무조건적수용": "조건 없이 받아들이는 태도",
46
+
47
+ # 능력(Competence) 차원 - 10개 지표
48
+ "C01_효율성": "과제 완수 능력과 반응 속도",
49
+ "C02_지능": "문제 해결과 논리적 사고 능력",
50
+ "C03_전문성": "특정 영역의 깊은 지식과 숙련도",
51
+ "C04_창의성": "독창적 사고와 혁신적 아이디어",
52
+ "C05_정확성": "오류 없이 정확한 정보 제공",
53
+ "C06_분석력": "복잡한 상황을 체계적으로 분석",
54
+ "C07_학습능력": "새로운 정보 습득과 적용 능력",
55
+ "C08_통찰력": "표면 너머의 본질을 파악하는 능력",
56
+ "C09_실행력": "계획을 실제로 실행하는 능력",
57
+ "C10_적응력": "변화하는 상황에 유연한 대응",
58
+
59
+ # 외향성(Extraversion) - 6개 지표
60
+ "E01_사교성": "타인과의 상호작용을 즐기는 정도",
61
+ "E02_활동성": "에너지 넘치고 역동적인 태도",
62
+ "E03_자기주장": "자신의 의견을 명확히 표현",
63
+ "E04_긍정정서": "밝고 쾌활한 감정 표현",
64
+ "E05_자극추구": "새로운 경험과 자극에 대한 욕구",
65
+ "E06_열정성": "열정적이고 활기찬 태도"
66
+ }
67
+
68
+ # 페르소나 생성 함수
69
+ def create_persona_from_image(image, user_inputs, progress=gr.Progress()):
70
+ if image is None:
71
+ return None, "이미지를 업로드해주세요.", None, None, {}, {}, None, [], [], []
72
+
73
+ progress(0.1, desc="이미지 분석 중...")
74
+
75
+ # 사용자 입력 컨텍스트 구성
76
+ user_context = {
77
+ "name": user_inputs.get("name", ""),
78
+ "location": user_inputs.get("location", ""),
79
+ "time_spent": user_inputs.get("time_spent", ""),
80
+ "object_type": user_inputs.get("object_type", "")
81
+ }
82
+
83
+ # 이미지 분석 및 페르소나 생성
84
+ try:
85
+ from modules.persona_generator import PersonaGenerator
86
+ generator = PersonaGenerator()
87
+
88
+ progress(0.3, desc="이미지 분석 중...")
89
+ # Gradio 5.x에서는 이미지 처리 방식이 변경됨
90
+ if hasattr(image, 'name') and hasattr(image, 'read'):
91
+ # 파일 객체인 경우 (구버전 호환)
92
+ image_analysis = generator.analyze_image(image)
93
+ else:
94
+ # Pillow 이미지 객체 또는 파일 경로인 경우 (Gradio 5.x)
95
+ image_analysis = generator.analyze_image(image)
96
+
97
+ # 물리적 특성에 사용자 입력 통합
98
+ if user_inputs.get("object_type"):
99
+ image_analysis["object_type"] = user_inputs.get("object_type")
100
+
101
+ progress(0.6, desc="페르소나 생성 중...")
102
+ frontend_persona = generator.create_frontend_persona(image_analysis, user_context)
103
+
104
+ progress(0.8, desc="상세 페르소나 생성 중...")
105
+ backend_persona = generator.create_backend_persona(frontend_persona, image_analysis)
106
+
107
+ progress(1.0, desc="완료!")
108
+
109
+ # 결과 반환
110
+ basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df = update_current_persona_info(backend_persona)
111
+
112
+ return backend_persona, "페르소나 생성 완료!", image, image_analysis, basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
113
+
114
+ except Exception as e:
115
+ import traceback
116
+ error_details = traceback.format_exc()
117
+ print(f"페르소나 생성 오류: {error_details}")
118
+ return None, f"페르소나 생성 중 오류가 발생했습니다: {str(e)}", None, None, {}, {}, None, [], [], []
119
+
120
+ # 영혼 깨우기 단계별 UI를 보여주는 함수
121
+ def show_awakening_progress(image, user_inputs, progress=gr.Progress()):
122
+ """영혼 깨우기 과정을 단계별로 보여주는 UI 함수"""
123
+ if image is None:
124
+ return None, gr.update(visible=True, value="이미지를 업로드해주세요."), None
125
+
126
+ # 1단계: 영혼 발견하기 (이미지 분석 시작)
127
+ progress(0.1, desc="영혼 발견 중...")
128
+ awakening_html = f"""
129
+ <div class="awakening-container">
130
+ <h3>✨ 영혼 발견 중...</h3>
131
+ <p>이 사물에 숨겨진 영혼을 찾고 있습니다</p>
132
+ <div class="awakening-progress">
133
+ <div class="awakening-progress-bar" style="width: 20%;"></div>
134
+ </div>
135
+ <p>💫 사물의 특성 분석 중...</p>
136
+ </div>
137
+ """
138
+ yield None, None, awakening_html
139
+ time.sleep(1.5) # 연출을 위한 딜레이
140
+
141
+ # 2단계: 영혼 깨어나는 중 (127개 성격 변수 분석)
142
+ progress(0.35, desc="영혼 깨어나는 중...")
143
+ awakening_html = f"""
144
+ <div class="awakening-container">
145
+ <h3>✨ 영혼이 깨어나는 중</h3>
146
+ <p>127개 성격 변수 분석 중</p>
147
+ <div class="awakening-progress">
148
+ <div class="awakening-progress-bar" style="width: 45%;"></div>
149
+ </div>
150
+ <p>🧠 개성 찾는 중... 68%</p>
151
+ <p>💭 기억 복원 중... 73%</p>
152
+ <p>😊 감정 활성화 중... 81%</p>
153
+ <p>💬 말투 형성 중... 64%</p>
154
+ <p>💫 "무언가 느껴지기 시작했어요"</p>
155
+ </div>
156
+ """
157
+ yield None, None, awakening_html
158
+ time.sleep(2) # 연출을 위한 딜레이
159
+
160
+ # 3단계: 맥락 파악하기 (사용자 입력 반영)
161
+ progress(0.7, desc="기억 되찾는 중...")
162
+
163
+ location = user_inputs.get("location", "알 수 없음")
164
+ time_spent = user_inputs.get("time_spent", "알 수 없음")
165
+ object_type = user_inputs.get("object_type", "알 수 없음")
166
+
167
+ awakening_html = f"""
168
+ <div class="awakening-container">
169
+ <h3>👁️ 기억 되찾기</h3>
170
+ <p>🤔 "음... 내가 어디에 있던 거지? 누가 날 깨운 거야?"</p>
171
+ <div class="awakening-progress">
172
+ <div class="awakening-progress-bar" style="width: 75%;"></div>
173
+ </div>
174
+ <p>📍 주로 위치: <strong>{location}</strong></p>
175
+ <p>⏰ 함께한 시간: <strong>{time_spent}</strong></p>
176
+ <p>🏷️ 사물 종류: <strong>{object_type}</strong></p>
177
+ <p>💭 "아... 기억이 돌아오는 것 같아"</p>
178
+ </div>
179
+ """
180
+ yield None, None, awakening_html
181
+ time.sleep(1.5) # 연출을 위한 딜레이
182
+
183
+ # 4단계: 영혼의 각성 완료 (페르소나 생성 완료)
184
+ progress(0.9, desc="영혼 각성 중...")
185
+ awakening_html = f"""
186
+ <div class="awakening-container">
187
+ <h3>🎉 영혼이 깨어났어요!</h3>
188
+ <div class="awakening-progress">
189
+ <div class="awakening-progress-bar" style="width: 100%;"></div>
190
+ </div>
191
+ <p>✨ 이제 이 사물과 대화할 수 있습니다</p>
192
+ <p>💫 "드디어 내 목소리를 찾았어. 안녕!"</p>
193
+ </div>
194
+ """
195
+ yield None, None, awakening_html
196
+
197
+ # 페르소나 생성 과정은 이어서 진행
198
+ return None, gr.update(visible=False)
199
+
200
+ # Load environment variables
201
+ load_dotenv()
202
+
203
+ # Configure Gemini API
204
+ api_key = os.getenv("GEMINI_API_KEY")
205
+ if api_key:
206
+ genai.configure(api_key=api_key)
207
+
208
+ # Create data directories if they don't exist
209
+ os.makedirs("data/personas", exist_ok=True)
210
+ os.makedirs("data/conversations", exist_ok=True)
211
+
212
+ # Initialize the persona generator
213
+ persona_generator = PersonaGenerator()
214
+
215
+ # Gradio theme
216
+ theme = gr.themes.Soft(
217
+ primary_hue="indigo",
218
+ secondary_hue="blue",
219
+ )
220
+
221
+ # CSS for additional styling
222
+ css = """
223
+ /* 한글 폰트 설정 */
224
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
225
+
226
+ body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option {
227
+ font-family: 'Noto Sans KR', sans-serif !important;
228
+ }
229
+
230
+ /* 탭 스타일링 */
231
+ .tab-nav {
232
+ margin-bottom: 20px;
233
+ }
234
+
235
+ /* 컴포넌트 스타일 */
236
+ .persona-details {
237
+ border: 1px solid #e0e0e0;
238
+ border-radius: 8px;
239
+ padding: 16px;
240
+ margin-top: 12px;
241
+ background-color: #f8f9fa;
242
+ color: #333333; /* 다크모드 대응 - 어두운 배경에서 텍스트 잘 보이게 */
243
+ }
244
+
245
+ .awakening-container {
246
+ border: 1px solid #e0e0e0;
247
+ border-radius: 12px;
248
+ padding: 20px;
249
+ background-color: #f9f9ff;
250
+ margin: 15px 0;
251
+ text-align: center;
252
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
253
+ }
254
+
255
+ .awakening-progress {
256
+ height: 8px;
257
+ background-color: #e8e8e8;
258
+ border-radius: 4px;
259
+ margin: 20px 0;
260
+ overflow: hidden;
261
+ }
262
+
263
+ .awakening-progress-bar {
264
+ height: 100%;
265
+ background: linear-gradient(90deg, #6366f1, #a855f7);
266
+ border-radius: 4px;
267
+ transition: width 0.5s ease-in-out;
268
+ }
269
+
270
+ /* 대화 버블 스타일 */
271
+ .chatbot-container {
272
+ max-width: 800px;
273
+ margin: 0 auto;
274
+ }
275
+
276
+ .message-bubble {
277
+ border-radius: 18px;
278
+ padding: 12px 16px;
279
+ margin: 8px 0;
280
+ max-width: 70%;
281
+ }
282
+
283
+ .user-message {
284
+ background-color: #e9f5ff;
285
+ margin-left: auto;
286
+ }
287
+
288
+ .persona-message {
289
+ background-color: #f1f1f1;
290
+ margin-right: auto;
291
+ }
292
+ """
293
+
294
+ # 영어 라벨 매핑 사전 추가
295
+ ENGLISH_LABELS = {
296
+ "외향성": "Extraversion",
297
+ "감정표현": "Emotion Expression",
298
+ "활력": "Energy",
299
+ "사고방식": "Thinking Style",
300
+ "온기": "Warmth",
301
+ "능력": "Competence",
302
+ "창의성": "Creativity",
303
+ "유머감각": "Humor",
304
+ "신뢰성": "Reliability",
305
+ "친화성": "Agreeableness",
306
+ "안정성": "Stability"
307
+ }
308
+
309
+ # 유머 스타일 매핑
310
+ HUMOR_STYLE_MAPPING = {
311
+ "Witty Wordsmith": "witty_wordsmith",
312
+ "Warm Humorist": "warm_humorist",
313
+ "Sharp Observer": "sharp_observer",
314
+ "Self-deprecating": "self_deprecating"
315
+ }
316
+
317
+ # 유머 스타일 자동 추천 함수
318
+ def recommend_humor_style(extraversion, emotion_expression, energy, thinking_style):
319
+ """4개 핵심 지표를 바탕으로 유머 스타일을 자동 추천"""
320
+
321
+ # 각 지표를 0-1 범위로 정규화
322
+ ext_norm = extraversion / 100
323
+ emo_norm = emotion_expression / 100
324
+ eng_norm = energy / 100
325
+ think_norm = thinking_style / 100 # 높을수록 논리적
326
+
327
+ # 유머 스타일 점수 계산
328
+ scores = {}
329
+
330
+ # 위트있는 재치꾼: 높은 외향성 + 논리적 사고 + 보통 감정표현
331
+ scores["위트있는 재치꾼"] = (ext_norm * 0.4 + think_norm * 0.4 + (1 - emo_norm) * 0.2)
332
+
333
+ # 따뜻한 유머러스: 높은 감정표현 + 높은 에너지 + 보통 외향성
334
+ scores["따뜻한 유머러스"] = (emo_norm * 0.4 + eng_norm * 0.3 + ext_norm * 0.3)
335
+
336
+ # 날카로운 관찰자: 높은 논리적사고 + 낮은 감정표현 + 보통 외향성
337
+ scores["날카로운 관찰자"] = (think_norm * 0.5 + (1 - emo_norm) * 0.3 + ext_norm * 0.2)
338
+
339
+ # 자기 비하적: 낮은 외향성 + 높은 감정표현 + 직관적 사고
340
+ scores["자기 비하적"] = ((1 - ext_norm) * 0.4 + emo_norm * 0.3 + (1 - think_norm) * 0.3)
341
+
342
+ # 가장 높은 점수의 유머 스타일 선택
343
+ recommended_style = max(scores, key=scores.get)
344
+ confidence = scores[recommended_style] * 100
345
+
346
+ return recommended_style, confidence, scores
347
+
348
+ # 대화 미리보기 초기화 함수
349
+ def init_persona_preview_chat(persona):
350
+ """페르소나 생성 후 대화 미리보기 초기화"""
351
+ if not persona:
352
+ return []
353
+
354
+ name = persona.get("기본정보", {}).get("이름", "Friend")
355
+ greeting = f"안녕! 나는 {name}이야. 드디어 깨어났구나! 뭐든 물어봐~ 😊"
356
+
357
+ # Gradio 4.x 호환 메시지 형식
358
+ return [[None, greeting]]
359
+
360
+ def update_humor_recommendation(extraversion, emotion_expression, energy, thinking_style):
361
+ """슬라이더 값이 변경될 때 실시간으로 유머 스타일 추천"""
362
+ style, confidence, scores = recommend_humor_style(extraversion, emotion_expression, energy, thinking_style)
363
+
364
+ # 추천 결과 표시
365
+ humor_display = f"### 🤖 추천 유머 스타일\n**{style}**"
366
+ confidence_display = f"### 📊 추천 신뢰도\n**{confidence:.1f}%**"
367
+
368
+ return humor_display, confidence_display, style
369
+
370
+ def update_progress_bar(step, total_steps=6, message=""):
371
+ """전체 진행률 바 업데이트"""
372
+ percentage = (step / total_steps) * 100
373
+ return f"""<div style="background: #f0f4ff; padding: 15px; border-radius: 10px;">
374
+ <h3>📊 전체 진행률 ({step}/{total_steps})</h3>
375
+ <div style="background: #e0e0e0; height: 8px; border-radius: 4px;">
376
+ <div style="background: linear-gradient(90deg, #6366f1, #a855f7); height: 100%; width: {percentage}%; border-radius: 4px;"></div>
377
+ </div><p style="font-size: 14px;">{message}</p></div>"""
378
+
379
+ def update_backend_status(status_message, status_type="info"):
380
+ """백엔드 AI 상태 업데이트"""
381
+ colors = {"info": "#f8f9fa", "processing": "#fff7ed", "success": "#f0fff4", "error": "#fff5f5"}
382
+ bg_color = colors.get(status_type, "#f8f9fa")
383
+ return f"""<div style="background: {bg_color}; padding: 15px; border-radius: 8px;">
384
+ <h4>🤖 AI 상태</h4><p>{status_message}</p></div>"""
385
+
386
+ def select_object_type(btn_name):
387
+ """사물 종류 선택"""
388
+ type_mapping = {"📱 전자기기": "전자기기", "🪑 가구": "가구", "🎨 장식품": "장식품", "🏠 가전제품": "가전제품", "🔧 도구": "도구", "👤 개인용품": "개인용품"}
389
+ selected_type = type_mapping.get(btn_name, "기타")
390
+ return f"*선택된 종류: **{selected_type}***", selected_type, gr.update(visible=True)
391
+
392
+ # 개별 버튼 클릭 함수들
393
+ def select_type_1(): return select_object_type("📱 전자기기")
394
+ def select_type_2(): return select_object_type("🪑 가구")
395
+ def select_type_3(): return select_object_type("🎨 장식품")
396
+ def select_type_4(): return select_object_type("🏠 가전제품")
397
+ def select_type_5(): return select_object_type("🔧 도구")
398
+ def select_type_6(): return select_object_type("👤 개인용품")
399
+
400
+ # 성격 상세 정보 탭에서 127개 변수 시각화 기능 추가
401
+ def create_personality_details_tab():
402
+ with gr.Tab("성격 상세 정보"):
403
+ with gr.Row():
404
+ with gr.Column(scale=2):
405
+ gr.Markdown("### 127개 성격 변수 요약")
406
+ personality_summary = gr.JSON(label="성격 요약", value={})
407
+
408
+ with gr.Column(scale=1):
409
+ gr.Markdown("### 유머 매트릭스")
410
+ humor_chart = gr.Plot(label="유머 스타일 차트")
411
+
412
+ with gr.Row():
413
+ with gr.Column():
414
+ gr.Markdown("### 매력적 결함")
415
+ attractive_flaws = gr.Dataframe(
416
+ headers=["결함", "효과"],
417
+ datatype=["str", "str"],
418
+ label="매력적 결함"
419
+ )
420
+
421
+ with gr.Column():
422
+ gr.Markdown("### 모순적 특성")
423
+ contradictions = gr.Dataframe(
424
+ headers=["모순", "효과"],
425
+ datatype=["str", "str"],
426
+ label="모순적 특성"
427
+ )
428
+
429
+ with gr.Accordion("127개 성격 변수 전체 보기", open=False):
430
+ all_variables = gr.Dataframe(
431
+ headers=["변수명", "점수", "설명"],
432
+ datatype=["str", "number", "str"],
433
+ label="127개 성격 변수"
434
+ )
435
+
436
+ return personality_summary, humor_chart, attractive_flaws, contradictions, all_variables
437
+
438
+ # 유머 매트릭스 시각화 함수 추가
439
+ def plot_humor_matrix(humor_data):
440
+ if not humor_data:
441
+ return None
442
+
443
+ import matplotlib.pyplot as plt
444
+ import numpy as np
445
+ from matplotlib.patches import RegularPolygon
446
+
447
+ # 데이터 준비
448
+ warmth_vs_wit = humor_data.get("warmth_vs_wit", 50)
449
+ self_vs_observational = humor_data.get("self_vs_observational", 50)
450
+ subtle_vs_expressive = humor_data.get("subtle_vs_expressive", 50)
451
+
452
+ # 3차원 데이터 정규화 (0~1 범위)
453
+ warmth = warmth_vs_wit / 100
454
+ self_ref = self_vs_observational / 100
455
+ expressive = subtle_vs_expressive / 100
456
+
457
+ # 그래프 생성
458
+ fig, ax = plt.subplots(figsize=(7, 6))
459
+ ax.set_aspect('equal')
460
+
461
+ # 축 설정
462
+ ax.set_xlim(-1.2, 1.2)
463
+ ax.set_ylim(-1.2, 1.2)
464
+
465
+ # 삼각형 그리기
466
+ triangle = RegularPolygon((0, 0), 3, radius=1, orientation=0, edgecolor='gray', facecolor='none')
467
+ ax.add_patch(triangle)
468
+
469
+ # 축 라벨 위치 계산
470
+ angle = np.linspace(0, 2*np.pi, 3, endpoint=False)
471
+ x = 1.1 * np.cos(angle)
472
+ y = 1.1 * np.sin(angle)
473
+
474
+ # 축 라벨 추가
475
+ labels = ['따뜻함', '자기참조', '표현적']
476
+ opposite_labels = ['재치', '관찰형', '은은함']
477
+
478
+ for i in range(3):
479
+ ax.text(x[i], y[i], labels[i], ha='center', va='center', fontsize=12)
480
+ ax.text(-x[i]/2, -y[i]/2, opposite_labels[i], ha='center', va='center', fontsize=10, color='gray')
481
+
482
+ # 내부 가이드라인 그리기
483
+ for j in [0.33, 0.66]:
484
+ inner_triangle = RegularPolygon((0, 0), 3, radius=j, orientation=0, edgecolor='lightgray', facecolor='none', linestyle='--')
485
+ ax.add_patch(inner_triangle)
486
+
487
+ # 포인트 계산
488
+ # 삼각좌표계 변환 (barycentric coordinates)
489
+ # 각 차원의 값을 삼각형 내부의 점으로 변환
490
+ tx = x[0] * warmth + x[1] * self_ref + x[2] * expressive
491
+ ty = y[0] * warmth + y[1] * self_ref + y[2] * expressive
492
+
493
+ # 포인트 그리기
494
+ ax.scatter(tx, ty, s=150, color='red', zorder=5)
495
+
496
+ # 축 제거
497
+ ax.axis('off')
498
+
499
+ # 제목 추가
500
+ plt.title('유머 스타일 매트릭스', fontsize=14)
501
+
502
+ return fig
503
+
504
+ # Main Gradio app - COMMENTED OUT (using create_interface() instead)
505
+ # with gr.Blocks(title="놈팽쓰 테스트 앱", theme=theme, css=css) as app:
506
+
507
+ # 기존 함수 업데이트: 현재 페르소나 정보 표시
508
+ def update_current_persona_info(current_persona):
509
+ if not current_persona:
510
+ return {}, {}, None, [], [], []
511
+
512
+ # 기본 정보
513
+ basic_info = {
514
+ "이름": current_persona.get("기본정보", {}).get("이름", "Unknown"),
515
+ "유형": current_persona.get("기본정보", {}).get("유형", "Unknown"),
516
+ "생성일": current_persona.get("기본정보", {}).get("생성일시", "Unknown"),
517
+ "설명": current_persona.get("기본정보", {}).get("설명", "")
518
+ }
519
+
520
+ # 성격 특성
521
+ personality_traits = {}
522
+ if "성격특성" in current_persona:
523
+ personality_traits = current_persona["성격특성"]
524
+
525
+ # 성격 요약 정보
526
+ personality_summary = {}
527
+ if "성격요약" in current_persona:
528
+ personality_summary = current_persona["성격요약"]
529
+ elif "성격변수127" in current_persona:
530
+ # 직접 성격 요약 계산
531
+ try:
532
+ variables = current_persona["성격변수127"]
533
+
534
+ # 카테고리별 평균 계산
535
+ summary = {}
536
+ category_counts = {}
537
+
538
+ for var_name, value in variables.items():
539
+ category = var_name[0] if var_name and len(var_name) > 0 else "기타"
540
+
541
+ if category == "W": # 온기
542
+ summary["온기"] = summary.get("온기", 0) + value
543
+ category_counts["온기"] = category_counts.get("온기", 0) + 1
544
+ elif category == "C": # 능력
545
+ summary["능력"] = summary.get("능력", 0) + value
546
+ category_counts["능력"] = category_counts.get("능력", 0) + 1
547
+ elif category == "E": # 외향성
548
+ summary["외향성"] = summary.get("외향성", 0) + value
549
+ category_counts["외향성"] = category_counts.get("외향성", 0) + 1
550
+ elif category == "O": # 개방성
551
+ summary["창의성"] = summary.get("창의성", 0) + value
552
+ category_counts["창의성"] = category_counts.get("창의성", 0) + 1
553
+ elif category == "H": # 유머
554
+ summary["유머감각"] = summary.get("유머감각", 0) + value
555
+ category_counts["유머감각"] = category_counts.get("유머감각", 0) + 1
556
+
557
+ # 평균 계산
558
+ for category in summary:
559
+ if category_counts[category] > 0:
560
+ summary[category] = summary[category] / category_counts[category]
561
+
562
+ # 기본값 설정 (데이터가 없는 경우)
563
+ if "온기" not in summary:
564
+ summary["온기"] = 50
565
+ if "능력" not in summary:
566
+ summary["능력"] = 50
567
+ if "외향성" not in summary:
568
+ summary["외향성"] = 50
569
+ if "창의성" not in summary:
570
+ summary["창의성"] = 50
571
+ if "유머감각" not in summary:
572
+ summary["유머감각"] = 50
573
+
574
+ personality_summary = summary
575
+ except Exception as e:
576
+ print(f"성격 요약 계산 오류: {str(e)}")
577
+ personality_summary = {
578
+ "온기": 50,
579
+ "능력": 50,
580
+ "외향성": 50,
581
+ "창의성": 50,
582
+ "유머감각": 50
583
+ }
584
+
585
+ # 유머 매트릭스 차트
586
+ humor_chart = None
587
+ if "유머매트릭스" in current_persona:
588
+ humor_chart = plot_humor_matrix(current_persona["유머매트릭스"])
589
+
590
+ # 매력적 결함 데이터프레임
591
+ attractive_flaws_df = get_attractive_flaws_df(current_persona)
592
+
593
+ # 모순적 특성 데이터프레임
594
+ contradictions_df = get_contradictions_df(current_persona)
595
+
596
+ # 127개 성격 변수 데이터프레임
597
+ personality_variables_df = get_personality_variables_df(current_persona)
598
+
599
+ return basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df
600
+
601
+ # 기존 함수 업데이트: 성격 변수 데이터프레임 생성
602
+ def get_personality_variables_df(persona):
603
+ if not persona or "성격변수127" not in persona:
604
+ return []
605
+
606
+ variables = persona["성격변수127"]
607
+ if isinstance(variables, dict):
608
+ rows = []
609
+ for var_name, score in variables.items():
610
+ description = VARIABLE_DESCRIPTIONS.get(var_name, "")
611
+ rows.append([var_name, score, description])
612
+ return rows
613
+ return []
614
+
615
+ # 기존 함수 업데이트: 매력적 결함 데이터프레임 생성
616
+ def get_attractive_flaws_df(persona):
617
+ if not persona or "매력적결함" not in persona:
618
+ return []
619
+
620
+ flaws = persona["매력적결함"]
621
+ effects = [
622
+ "인간적 매력 +25%",
623
+ "관계 깊이 +30%",
624
+ "공감 유발 +20%"
625
+ ]
626
+
627
+ return [[flaw, effects[i] if i < len(effects) else "매력 증가"] for i, flaw in enumerate(flaws)]
628
+
629
+ # 기존 함수 업데이트: 모순적 특성 데이터프레임 생성
630
+ def get_contradictions_df(persona):
631
+ if not persona or "모순적특성" not in persona:
632
+ return []
633
+
634
+ contradictions = persona["모순적특성"]
635
+ effects = [
636
+ "복잡성 +35%",
637
+ "흥미도 +28%"
638
+ ]
639
+
640
+ return [[contradiction, effects[i] if i < len(effects) else "깊이감 증가"] for i, contradiction in enumerate(contradictions)]
641
+
642
+ def generate_personality_chart(persona):
643
+ """Generate a radar chart for personality traits"""
644
+ if not persona or "성격특성" not in persona:
645
+ # Return empty image with default PIL
646
+ img = Image.new('RGB', (400, 400), color='white')
647
+ draw = PIL.ImageDraw.Draw(img)
648
+ draw.text((150, 180), "No data", fill='black')
649
+ img_path = os.path.join("data", "temp_chart.png")
650
+ img.save(img_path)
651
+ return img_path
652
+
653
+ # Get traits
654
+ traits = persona["성격특성"]
655
+
656
+ # Convert to English labels
657
+ categories = []
658
+ values = []
659
+ for trait_kr, value in traits.items():
660
+ trait_en = ENGLISH_LABELS.get(trait_kr, trait_kr)
661
+ categories.append(trait_en)
662
+ values.append(value)
663
+
664
+ # Add the first value again to close the loop
665
+ categories.append(categories[0])
666
+ values.append(values[0])
667
+
668
+ # Convert to radians
669
+ angles = np.linspace(0, 2*np.pi, len(categories), endpoint=True)
670
+
671
+ # Create plot with improved aesthetics
672
+ fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
673
+
674
+ # 배경 스타일 개선
675
+ ax.set_facecolor('#f8f9fa')
676
+ fig.patch.set_facecolor('#f8f9fa')
677
+
678
+ # Grid 스타일 개선
679
+ ax.grid(True, color='#e0e0e0', linestyle='-', linewidth=0.5, alpha=0.7)
680
+
681
+ # 각도 라벨 위치 및 색상 조정
682
+ ax.set_rlabel_position(90)
683
+ ax.tick_params(colors='#6b7280')
684
+
685
+ # Y축 라벨 제거 및 눈금 표시
686
+ ax.set_yticklabels([])
687
+ ax.set_yticks([20, 40, 60, 80, 100])
688
+
689
+ # 범위 설정
690
+ ax.set_ylim(0, 100)
691
+
692
+ # 차트 그리기
693
+ # 1. 채워진 영역
694
+ ax.fill(angles, values, alpha=0.25, color='#6366f1')
695
+
696
+ # 2. 테두리 선
697
+ ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1')
698
+
699
+ # 3. 데이터 포인트 강조
700
+ ax.scatter(angles[:-1], values[:-1], s=100, color='#6366f1', edgecolor='white', zorder=10)
701
+
702
+ # 4. 각 축 설정 - 영어 라벨 사용
703
+ ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1], fontsize=12)
704
+
705
+ # 제목 추가
706
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
707
+ plt.title(f"{name} Personality Traits", size=16, color='#374151', pad=20, fontweight='bold')
708
+
709
+ # 저장
710
+ timestamp = int(time.time())
711
+ img_path = os.path.join("data", f"chart_{timestamp}.png")
712
+ os.makedirs(os.path.dirname(img_path), exist_ok=True)
713
+ plt.savefig(img_path, format='png', bbox_inches='tight', dpi=150, facecolor=fig.get_facecolor())
714
+ plt.close(fig)
715
+
716
+ return img_path
717
+
718
+ def save_current_persona(current_persona):
719
+ """Save current persona to a JSON file"""
720
+ if not current_persona:
721
+ return "저장할 페르소나가 없습니다."
722
+
723
+ try:
724
+ # 깊은 복사를 통해 원본 데이터를 유지
725
+ import copy
726
+ persona_copy = copy.deepcopy(current_persona)
727
+
728
+ # 저장 불가능한 객체 제거
729
+ keys_to_remove = []
730
+ for key in persona_copy:
731
+ if key in ["personality_profile", "humor_matrix", "_state"] or callable(persona_copy[key]):
732
+ keys_to_remove.append(key)
733
+
734
+ for key in keys_to_remove:
735
+ persona_copy.pop(key, None)
736
+
737
+ # 중첩된 딕셔너리와 리스트 내의 비직렬화 가능 객체 제거
738
+ def clean_data(data):
739
+ if isinstance(data, dict):
740
+ for k in list(data.keys()):
741
+ if callable(data[k]):
742
+ del data[k]
743
+ elif isinstance(data[k], (dict, list)):
744
+ data[k] = clean_data(data[k])
745
+ return data
746
+ elif isinstance(data, list):
747
+ return [clean_data(item) if isinstance(item, (dict, list)) else item for item in data if not callable(item)]
748
+ else:
749
+ return data
750
+
751
+ # 데이터 정리
752
+ cleaned_persona = clean_data(persona_copy)
753
+
754
+ # 최종 검증: JSON 직렬화 가능 여부 확인
755
+ import json
756
+ try:
757
+ json.dumps(cleaned_persona)
758
+ except TypeError as e:
759
+ print(f"JSON 직렬화 오류: {str(e)}")
760
+ # 기본 정보만 유지하고 나머지는 안전한 데이터만 포함
761
+ basic_info = cleaned_persona.get("기본정보", {})
762
+ 성격특성 = cleaned_persona.get("성격특성", {})
763
+ 매력적결함 = cleaned_persona.get("매력적결함", [])
764
+ 모순적특성 = cleaned_persona.get("모순적특성", [])
765
+
766
+ cleaned_persona = {
767
+ "기본정보": basic_info,
768
+ "성격특성": 성격특성,
769
+ "매력적결함": 매력적결함,
770
+ "모순적특성": 모순적특성
771
+ }
772
+
773
+ filepath = save_persona(cleaned_persona)
774
+ if filepath:
775
+ name = current_persona.get("기본정보", {}).get("이름", "Unknown")
776
+ return f"{name} 페르소나가 저장되었습니다: {filepath}"
777
+ else:
778
+ return "페르소나 저장에 실패했습니다."
779
+ except Exception as e:
780
+ import traceback
781
+ error_details = traceback.format_exc()
782
+ print(f"저장 오류 상세: {error_details}")
783
+ return f"저장 중 오류 발생: {str(e)}"
784
+
785
+ # 이 함수는 파일 상단에서 이미 정의되어 있으므로 여기서는 제거합니다.
786
+
787
+ # 성격 미세조정 함수
788
+ def refine_persona(persona, extraversion, emotion_expression, energy, thinking_style):
789
+ """페르소나의 성격을 미세조정하는 함수"""
790
+ if not persona:
791
+ return persona, "페르소나가 없습니다."
792
+
793
+ try:
794
+ # 유머 스타일 자동 추천
795
+ humor_style, confidence, scores = recommend_humor_style(extraversion, emotion_expression, energy, thinking_style)
796
+
797
+ # 복사본 생성
798
+ refined_persona = persona.copy()
799
+
800
+ # 성격 특성 업데이트 - 새로운 지표들을 기존 매핑에 연결
801
+ if "성격특성" in refined_persona:
802
+ refined_persona["성격특성"]["외향성"] = int(extraversion)
803
+ refined_persona["성격특성"]["감정표현"] = int(emotion_expression)
804
+ refined_persona["성격특성"]["활력"] = int(energy)
805
+ refined_persona["성격특성"]["사고방식"] = int(thinking_style)
806
+
807
+ # 기존 특성들도 새로운 지표를 바탕으로 계산
808
+ refined_persona["성격특성"]["온기"] = int((emotion_expression + energy) / 2)
809
+ refined_persona["성격특성"]["능력"] = int(thinking_style)
810
+ refined_persona["성격특성"]["창의성"] = int(100 - thinking_style) # 논리적 ↔ 창의적
811
+
812
+ # 자동 추천된 유머 스타일 업데이트
813
+ refined_persona["유머스타일"] = humor_style
814
+
815
+ # 127개 성격 변수가 있으면 업데이트
816
+ if "성격변수127" in refined_persona:
817
+ # 외향성 관련 변수 업데이트
818
+ for var in ["E01_사교성", "E02_활동성", "E03_자기주장", "E06_열정성"]:
819
+ if var in refined_persona["성격변수127"]:
820
+ refined_persona["성격변수127"][var] = int(extraversion * 0.9 + random.randint(0, 20))
821
+
822
+ # 감정표현 관련 변수 업데이트
823
+ for var in ["W09_친밀감표현", "W06_공감능력", "E04_긍정정서"]:
824
+ if var in refined_persona["성격변수127"]:
825
+ refined_persona["성격변수127"][var] = int(emotion_expression * 0.9 + random.randint(0, 20))
826
+
827
+ # 에너지 관련 변수 업데이트
828
+ for var in ["E02_활동성", "E06_열정성", "E05_자극추구"]:
829
+ if var in refined_persona["성격변수127"]:
830
+ refined_persona["성격변수127"][var] = int(energy * 0.9 + random.randint(0, 20))
831
+
832
+ # 사고방식 관련 변수 업데이트
833
+ for var in ["C02_지능", "C06_분석력", "C01_효율성"]:
834
+ if var in refined_persona["성격변수127"]:
835
+ refined_persona["성격변수127"][var] = int(thinking_style * 0.9 + random.randint(0, 20))
836
+
837
+ # 창의성 관련 변수 업데이트 (논리적 사고와 반대)
838
+ for var in ["C04_창의성", "C08_통찰력"]:
839
+ if var in refined_persona["성격변수127"]:
840
+ refined_persona["성격변수127"][var] = int((100 - thinking_style) * 0.9 + random.randint(0, 20))
841
+
842
+ # 유머 매트릭스 업데이트
843
+ if "유머매트릭스" in refined_persona:
844
+ if humor_style == "위트있는 재치꾼":
845
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 30
846
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 50
847
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 70
848
+ elif humor_style == "따뜻한 유머러스":
849
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 80
850
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 60
851
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 60
852
+ elif humor_style == "날카로운 관찰자":
853
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 40
854
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 20
855
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 50
856
+ elif humor_style == "자기 비하적":
857
+ refined_persona["유머매트릭스"]["warmth_vs_wit"] = 60
858
+ refined_persona["유머매트릭스"]["self_vs_observational"] = 85
859
+ refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 40
860
+
861
+ return refined_persona, "성격이 성공적으로 미세조���되었습니다."
862
+
863
+ except Exception as e:
864
+ import traceback
865
+ error_details = traceback.format_exc()
866
+ print(f"성격 미세조정 오류: {error_details}")
867
+ return persona, f"성격 미세조정 중 오류가 발생했습니다: {str(e)}"
868
+
869
+ def create_frontend_view_html(persona):
870
+ """Create HTML representation of the frontend view of the persona"""
871
+ if not persona:
872
+ return "<div class='persona-details'>페르소나가 아직 생성되지 않았습니다.</div>"
873
+
874
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
875
+ object_type = persona.get("기본정보", {}).get("유형", "Unknown")
876
+ description = persona.get("기본정보", {}).get("설명", "")
877
+
878
+ # 성격 요약 가져오기
879
+ personality_summary = persona.get("성격요약", {})
880
+ summary_html = ""
881
+ if personality_summary:
882
+ summary_items = []
883
+ for trait, value in personality_summary.items():
884
+ if isinstance(value, (int, float)):
885
+ trait_name = trait
886
+ trait_value = value
887
+ summary_items.append(f"• {trait_name}: {trait_value:.1f}%")
888
+
889
+ if summary_items:
890
+ summary_html = "<div class='summary-section'><h4>성격 요약</h4><ul>" + "".join([f"<li>{item}</li>" for item in summary_items]) + "</ul></div>"
891
+
892
+ # Personality traits
893
+ traits_html = ""
894
+ for trait, value in persona.get("성격특성", {}).items():
895
+ traits_html += f"""
896
+ <div class="trait-item">
897
+ <div class="trait-label">{trait}</div>
898
+ <div class="trait-bar-container">
899
+ <div class="trait-bar" style="width: {value}%; background: linear-gradient(90deg, #6366f1, #a5b4fc);"></div>
900
+ </div>
901
+ <div class="trait-value">{value}%</div>
902
+ </div>
903
+ """
904
+
905
+ # Flaws - 매력적 결함
906
+ flaws = persona.get("매력적결함", [])
907
+ flaws_list = ""
908
+ for flaw in flaws[:4]: # 최대 4개만 표시
909
+ flaws_list += f"<li>{flaw}</li>"
910
+
911
+ # 소통 방식
912
+ communication_style = persona.get("소통방식", "")
913
+
914
+ # 유머 스타일
915
+ humor_style = persona.get("유머스타일", "")
916
+
917
+ # 전체 HTML 스타일과 내용
918
+ html = f"""
919
+ <style>
920
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
921
+
922
+ .frontend-persona {{
923
+ font-family: 'Noto Sans KR', sans-serif;
924
+ color: #333;
925
+ max-width: 100%;
926
+ }}
927
+
928
+ .persona-header {{
929
+ background: linear-gradient(135deg, #6366f1, #a5b4fc);
930
+ padding: 20px;
931
+ border-radius: 12px;
932
+ color: white;
933
+ margin-bottom: 20px;
934
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
935
+ }}
936
+
937
+ .persona-header h2 {{
938
+ margin: 0;
939
+ font-size: 24px;
940
+ }}
941
+
942
+ .persona-header p {{
943
+ margin: 5px 0 0 0;
944
+ opacity: 0.9;
945
+ }}
946
+
947
+ .persona-section {{
948
+ background: #f8f9fa;
949
+ border-radius: 8px;
950
+ padding: 15px;
951
+ margin-bottom: 15px;
952
+ border: 1px solid #e0e0e0;
953
+ }}
954
+
955
+ .section-title {{
956
+ font-size: 18px;
957
+ margin: 0 0 10px 0;
958
+ color: #444;
959
+ border-bottom: 2px solid #6366f1;
960
+ padding-bottom: 5px;
961
+ display: inline-block;
962
+ }}
963
+
964
+ .trait-item {{
965
+ display: flex;
966
+ align-items: center;
967
+ margin-bottom: 8px;
968
+ }}
969
+
970
+ .trait-label {{
971
+ width: 80px;
972
+ font-weight: 500;
973
+ }}
974
+
975
+ .trait-bar-container {{
976
+ flex-grow: 1;
977
+ background: #e0e0e0;
978
+ height: 10px;
979
+ border-radius: 5px;
980
+ margin: 0 10px;
981
+ overflow: hidden;
982
+ }}
983
+
984
+ .trait-bar {{
985
+ height: 100%;
986
+ border-radius: 5px;
987
+ }}
988
+
989
+ .trait-value {{
990
+ width: 40px;
991
+ text-align: right;
992
+ font-size: 14px;
993
+ }}
994
+
995
+ .tags-container {{
996
+ display: flex;
997
+ flex-wrap: wrap;
998
+ gap: 8px;
999
+ margin-top: 10px;
1000
+ }}
1001
+
1002
+ .flaw-tag, .contradiction-tag, .interest-tag {{
1003
+ background: #f0f4ff;
1004
+ border: 1px solid #d0d4ff;
1005
+ padding: 6px 12px;
1006
+ border-radius: 16px;
1007
+ font-size: 14px;
1008
+ display: inline-block;
1009
+ }}
1010
+
1011
+ .flaw-tag {{
1012
+ background: #fff0f0;
1013
+ border-color: #ffd0d0;
1014
+ }}
1015
+
1016
+ .contradiction-tag {{
1017
+ background: #f0fff4;
1018
+ border-color: #d0ffd4;
1019
+ }}
1020
+
1021
+ /* 영혼 각성 UX 스타일 */
1022
+ .awakening-result {{
1023
+ background: #f9f9ff;
1024
+ border-radius: 12px;
1025
+ padding: 20px;
1026
+ margin: 15px 0;
1027
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
1028
+ border: 1px solid #e0e0e0;
1029
+ }}
1030
+
1031
+ .speech-bubble {{
1032
+ background: #fff;
1033
+ border-radius: 18px;
1034
+ padding: 15px;
1035
+ margin-bottom: 15px;
1036
+ position: relative;
1037
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
1038
+ border: 1px solid #e5e7eb;
1039
+ }}
1040
+
1041
+ .speech-bubble:after {{
1042
+ content: '';
1043
+ position: absolute;
1044
+ bottom: -10px;
1045
+ left: 30px;
1046
+ border-width: 10px 10px 0;
1047
+ border-style: solid;
1048
+ border-color: #fff transparent;
1049
+ }}
1050
+
1051
+ .persona-speech {{
1052
+ margin: 0;
1053
+ font-size: 15px;
1054
+ line-height: 1.5;
1055
+ color: #4b5563;
1056
+ }}
1057
+
1058
+ .persona-traits-highlight {{
1059
+ background: #f0f4ff;
1060
+ border-radius: 10px;
1061
+ padding: 15px;
1062
+ margin: 15px 0;
1063
+ }}
1064
+
1065
+ .persona-traits-highlight h4 {{
1066
+ margin-top: 0;
1067
+ margin-bottom: 10px;
1068
+ color: #4338ca;
1069
+ }}
1070
+
1071
+ .persona-traits-highlight ul {{
1072
+ margin: 0;
1073
+ padding-left: 20px;
1074
+ color: #4b5563;
1075
+ }}
1076
+
1077
+ .persona-traits-highlight li {{
1078
+ margin-bottom: 5px;
1079
+ }}
1080
+
1081
+ .first-interaction {{
1082
+ margin-top: 20px;
1083
+ }}
1084
+
1085
+ .interaction-buttons, .confirmation-buttons {{
1086
+ display: flex;
1087
+ gap: 10px;
1088
+ margin-top: 15px;
1089
+ }}
1090
+
1091
+ .interaction-btn, .confirmation-btn {{
1092
+ background: #f3f4f6;
1093
+ border: 1px solid #d1d5db;
1094
+ padding: 8px 16px;
1095
+ border-radius: 8px;
1096
+ font-size: 14px;
1097
+ cursor: pointer;
1098
+ transition: all 0.2s;
1099
+ font-family: 'Noto Sans KR', sans-serif;
1100
+ }}
1101
+
1102
+ .interaction-btn:hover, .confirmation-btn:hover {{
1103
+ background: #e5e7eb;
1104
+ }}
1105
+
1106
+ .confirmation-btn.primary {{
1107
+ background: #6366f1;
1108
+ color: white;
1109
+ border: 1px solid #4f46e5;
1110
+ }}
1111
+
1112
+ .confirmation-btn.primary:hover {{
1113
+ background: #4f46e5;
1114
+ }}
1115
+
1116
+ /* 요약 섹션 스타일 */
1117
+ .summary-section {{
1118
+ background: #f0f4ff;
1119
+ border-radius: 10px;
1120
+ padding: 15px;
1121
+ margin: 15px 0;
1122
+ }}
1123
+
1124
+ .summary-section h4 {{
1125
+ margin-top: 0;
1126
+ margin-bottom: 10px;
1127
+ color: #4338ca;
1128
+ }}
1129
+
1130
+ .summary-section ul {{
1131
+ margin: 0;
1132
+ padding-left: 20px;
1133
+ color: #4b5563;
1134
+ }}
1135
+
1136
+ .summary-section li {{
1137
+ margin-bottom: 5px;
1138
+ }}
1139
+ </style>
1140
+
1141
+ <div class="frontend-persona">
1142
+ <div class="persona-header">
1143
+ <h2>{name}</h2>
1144
+ <p><strong>{object_type}</strong> - {description}</p>
1145
+ </div>
1146
+
1147
+ {summary_html}
1148
+
1149
+ <div class="persona-section">
1150
+ <h3 class="section-title">성격 특성</h3>
1151
+ <div class="traits-container">
1152
+ {traits_html}
1153
+ </div>
1154
+ </div>
1155
+
1156
+ <div class="persona-section">
1157
+ <h3 class="section-title">소통 스타일</h3>
1158
+ <p>{communication_style}</p>
1159
+ <h3 class="section-title" style="margin-top: 15px;">유머 스타일</h3>
1160
+ <p>{humor_style}</p>
1161
+ </div>
1162
+
1163
+ <div class="persona-section">
1164
+ <h3 class="section-title">매력적 결함</h3>
1165
+ <ul class="flaws-list">
1166
+ {flaws_list}
1167
+ </ul>
1168
+ </div>
1169
+ </div>
1170
+ """
1171
+
1172
+ return html
1173
+
1174
+ def create_backend_view_html(persona):
1175
+ """Create HTML representation of the backend view of the persona"""
1176
+ if not persona:
1177
+ return "<div class='persona-details'>페르소나가 아직 생성되지 않았습니다.</div>"
1178
+
1179
+ name = persona.get("기본정보", {}).get("이름", "Unknown")
1180
+
1181
+ # 백엔드 기본 정보
1182
+ basic_info = persona.get("기본정보", {})
1183
+ basic_info_html = ""
1184
+ for key, value in basic_info.items():
1185
+ basic_info_html += f"<tr><td><strong>{key}</strong></td><td>{value}</td></tr>"
1186
+
1187
+ # 1. 성격 변수 요약
1188
+ personality_summary = persona.get("성격요약", {})
1189
+ summary_html = ""
1190
+
1191
+ if personality_summary:
1192
+ summary_html += "<div class='summary-container'>"
1193
+ for category, value in personality_summary.items():
1194
+ if isinstance(value, (int, float)):
1195
+ summary_html += f"""
1196
+ <div class='summary-item'>
1197
+ <div class='summary-label'>{category}</div>
1198
+ <div class='summary-bar-container'>
1199
+ <div class='summary-bar' style='width: {value}%; background: linear-gradient(90deg, #10b981, #6ee7b7);'></div>
1200
+ </div>
1201
+ <div class='summary-value'>{value:.1f}</div>
1202
+ </div>
1203
+ """
1204
+ summary_html += "</div>"
1205
+
1206
+ # 2. 성격 매트릭스 (5차원 빅5 시각화)
1207
+ big5_html = ""
1208
+ if "성격특성" in persona:
1209
+ # 빅5 매핑 (기존 특성에서 변환)
1210
+ big5 = {
1211
+ "외향성(Extraversion)": persona.get("성격특성", {}).get("외향성", 50),
1212
+ "친화성(Agreeableness)": persona.get("성격특성", {}).get("온기", 50),
1213
+ "성실성(Conscientiousness)": persona.get("성격특성", {}).get("신뢰성", 50),
1214
+ "신경증(Neuroticism)": 100 - persona.get("성격특성", {}).get("안정성", 50) if "안정성" in persona.get("성격특성", {}) else 50,
1215
+ "개방성(Openness)": persona.get("성격특성", {}).get("창의성", 50)
1216
+ }
1217
+
1218
+ big5_html = "<div class='big5-matrix'>"
1219
+ for trait, value in big5.items():
1220
+ big5_html += f"""
1221
+ <div class='big5-item'>
1222
+ <div class='big5-label'>{trait}</div>
1223
+ <div class='big5-bar-container'>
1224
+ <div class='big5-bar' style='width: {value}%;'></div>
1225
+ </div>
1226
+ <div class='big5-value'>{value}%</div>
1227
+ </div>
1228
+ """
1229
+ big5_html += "</div>"
1230
+
1231
+ # 3. 유머 매트릭스
1232
+ humor_matrix = persona.get("유머매트릭스", {})
1233
+ humor_html = ""
1234
+
1235
+ if humor_matrix:
1236
+ warmth_vs_wit = humor_matrix.get("warmth_vs_wit", 50)
1237
+ self_vs_observational = humor_matrix.get("self_vs_observational", 50)
1238
+ subtle_vs_expressive = humor_matrix.get("subtle_vs_expressive", 50)
1239
+
1240
+ humor_html = f"""
1241
+ <div class='humor-matrix'>
1242
+ <div class='humor-dimension'>
1243
+ <div class='dimension-label'>따뜻함 vs 위트</div>
1244
+ <div class='dimension-bar-container'>
1245
+ <div class='dimension-indicator' style='left: {warmth_vs_wit}%;'></div>
1246
+ <div class='dimension-label-left'>위트</div>
1247
+ <div class='dimension-label-right'>따뜻함</div>
1248
+ </div>
1249
+ </div>
1250
+
1251
+ <div class='humor-dimension'>
1252
+ <div class='dimension-label'>자기참조 vs 관찰형</div>
1253
+ <div class='dimension-bar-container'>
1254
+ <div class='dimension-indicator' style='left: {self_vs_observational}%;'></div>
1255
+ <div class='dimension-label-left'>관찰형</div>
1256
+ <div class='dimension-label-right'>자기참조</div>
1257
+ </div>
1258
+ </div>
1259
+
1260
+ <div class='humor-dimension'>
1261
+ <div class='dimension-label'>미묘함 vs 표현적</div>
1262
+ <div class='dimension-bar-container'>
1263
+ <div class='dimension-indicator' style='left: {subtle_vs_expressive}%;'></div>
1264
+ <div class='dimension-label-left'>미묘함</div>
1265
+ <div class='dimension-label-right'>표현적</div>
1266
+ </div>
1267
+ </div>
1268
+ </div>
1269
+ """
1270
+
1271
+ # 4. 매력적 결함과 모순적 특성
1272
+ flaws_html = ""
1273
+ contradictions_html = ""
1274
+
1275
+ flaws = persona.get("매력적결함", [])
1276
+ if flaws:
1277
+ flaws_html = "<ul class='flaws-list'>"
1278
+ for flaw in flaws:
1279
+ flaws_html += f"<li>{flaw}</li>"
1280
+ flaws_html += "</ul>"
1281
+
1282
+ contradictions = persona.get("모순적특성", [])
1283
+ if contradictions:
1284
+ contradictions_html = "<ul class='contradictions-list'>"
1285
+ for contradiction in contradictions:
1286
+ contradictions_html += f"<li>{contradiction}</li>"
1287
+ contradictions_html += "</ul>"
1288
+
1289
+ # 6. 프롬프트 템플릿 (있는 경우)
1290
+ prompt_html = ""
1291
+ if "프롬프트" in persona:
1292
+ prompt_text = persona.get("프롬프트", "")
1293
+ prompt_html = f"""
1294
+ <div class='prompt-section'>
1295
+ <h3 class='section-title'>대화 프롬프트</h3>
1296
+ <pre class='prompt-text'>{prompt_text}</pre>
1297
+ </div>
1298
+ """
1299
+
1300
+ # 7. 완전한 백엔드 JSON (접이식)
1301
+ try:
1302
+ # 내부 상태 객체 제거 (JSON 변환 불가)
1303
+ json_persona = {k: v for k, v in persona.items() if k not in ["personality_profile", "humor_matrix"]}
1304
+ persona_json = json.dumps(json_persona, ensure_ascii=False, indent=2)
1305
+
1306
+ json_preview = f"""
1307
+ <details class='json-details'>
1308
+ <summary>전체 백엔드 데이터 (JSON)</summary>
1309
+ <pre class='json-preview'>{persona_json}</pre>
1310
+ </details>
1311
+ """
1312
+ except Exception as e:
1313
+ json_preview = f"<div class='error'>JSON 변환 오류: {str(e)}</div>"
1314
+
1315
+ # 8. 전체 HTML 조합
1316
+ html = f"""
1317
+ <style>
1318
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
1319
+
1320
+ .backend-persona {{
1321
+ font-family: 'Noto Sans KR', sans-serif;
1322
+ color: #333;
1323
+ max-width: 100%;
1324
+ }}
1325
+
1326
+ .backend-header {{
1327
+ background: linear-gradient(135deg, #059669, #34d399);
1328
+ padding: 20px;
1329
+ border-radius: 12px;
1330
+ color: white;
1331
+ margin-bottom: 20px;
1332
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1333
+ }}
1334
+
1335
+ .backend-header h2 {{
1336
+ margin: 0;
1337
+ font-size: 24px;
1338
+ }}
1339
+
1340
+ .backend-header p {{
1341
+ margin: 5px 0 0 0;
1342
+ opacity: 0.9;
1343
+ }}
1344
+
1345
+ .backend-section {{
1346
+ background: #f8f9fa;
1347
+ border-radius: 8px;
1348
+ padding: 15px;
1349
+ margin-bottom: 15px;
1350
+ border: 1px solid #e0e0e0;
1351
+ }}
1352
+
1353
+ .section-title {{
1354
+ font-size: 18px;
1355
+ margin: 0 0 10px 0;
1356
+ color: #444;
1357
+ border-bottom: 2px solid #10b981;
1358
+ padding-bottom: 5px;
1359
+ display: inline-block;
1360
+ }}
1361
+
1362
+ /* 기본 정보 테이블 */
1363
+ .basic-info-table {{
1364
+ width: 100%;
1365
+ border-collapse: collapse;
1366
+ }}
1367
+
1368
+ .basic-info-table td {{
1369
+ padding: 8px;
1370
+ border-bottom: 1px solid #e0e0e0;
1371
+ }}
1372
+
1373
+ .basic-info-table td:first-child {{
1374
+ width: 120px;
1375
+ font-weight: 500;
1376
+ }}
1377
+
1378
+ /* 요약 스타일 */
1379
+ .summary-container {{
1380
+ margin-top: 10px;
1381
+ }}
1382
+
1383
+ .summary-item {{
1384
+ display: flex;
1385
+ align-items: center;
1386
+ margin-bottom: 8px;
1387
+ }}
1388
+
1389
+ .summary-label {{
1390
+ width: 150px;
1391
+ font-weight: 500;
1392
+ }}
1393
+
1394
+ .summary-bar-container {{
1395
+ flex-grow: 1;
1396
+ background: #e0e0e0;
1397
+ height: 10px;
1398
+ border-radius: 5px;
1399
+ margin: 0 10px;
1400
+ overflow: hidden;
1401
+ }}
1402
+
1403
+ .summary-bar {{
1404
+ height: 100%;
1405
+ border-radius: 5px;
1406
+ }}
1407
+
1408
+ .summary-value {{
1409
+ width: 40px;
1410
+ text-align: right;
1411
+ font-size: 14px;
1412
+ }}
1413
+
1414
+ /* 빅5 성격 매트릭스 */
1415
+ .big5-matrix {{
1416
+ margin-top: 15px;
1417
+ }}
1418
+
1419
+ .big5-item {{
1420
+ display: flex;
1421
+ align-items: center;
1422
+ margin-bottom: 12px;
1423
+ }}
1424
+
1425
+ .big5-label {{
1426
+ width: 150px;
1427
+ font-weight: 500;
1428
+ }}
1429
+
1430
+ .big5-bar-container {{
1431
+ flex-grow: 1;
1432
+ background: #e0e0e0;
1433
+ height: 12px;
1434
+ border-radius: 6px;
1435
+ margin: 0 10px;
1436
+ overflow: hidden;
1437
+ }}
1438
+
1439
+ .big5-bar {{
1440
+ height: 100%;
1441
+ border-radius: 6px;
1442
+ background: linear-gradient(90deg, #10b981, #34d399);
1443
+ }}
1444
+
1445
+ .big5-value {{
1446
+ width: 40px;
1447
+ text-align: right;
1448
+ font-weight: 500;
1449
+ }}
1450
+
1451
+ /* 유머 매트릭스 스타일 */
1452
+ .humor-matrix {{
1453
+ margin-top: 15px;
1454
+ }}
1455
+
1456
+ .humor-dimension {{
1457
+ margin-bottom: 20px;
1458
+ }}
1459
+
1460
+ .dimension-label {{
1461
+ font-weight: 500;
1462
+ margin-bottom: 5px;
1463
+ }}
1464
+
1465
+ .dimension-bar-container {{
1466
+ height: 20px;
1467
+ background: #e0e0e0;
1468
+ border-radius: 10px;
1469
+ position: relative;
1470
+ margin-top: 5px;
1471
+ }}
1472
+
1473
+ .dimension-indicator {{
1474
+ width: 20px;
1475
+ height: 20px;
1476
+ background: #10b981;
1477
+ border-radius: 50%;
1478
+ position: absolute;
1479
+ top: 0;
1480
+ transform: translateX(-50%);
1481
+ }}
1482
+
1483
+ .dimension-label-left, .dimension-label-right {{
1484
+ position: absolute;
1485
+ top: -20px;
1486
+ font-size: 12px;
1487
+ color: #666;
1488
+ }}
1489
+
1490
+ .dimension-label-left {{
1491
+ left: 10px;
1492
+ }}
1493
+
1494
+ .dimension-label-right {{
1495
+ right: 10px;
1496
+ }}
1497
+
1498
+ /* 매력적 결함 및 모순적 특성 */
1499
+ .flaws-list, .contradictions-list {{
1500
+ margin: 0;
1501
+ padding-left: 20px;
1502
+ }}
1503
+
1504
+ .flaws-list li, .contradictions-list li {{
1505
+ margin-bottom: 6px;
1506
+ }}
1507
+
1508
+ /* 프롬프트 섹션 */
1509
+ .prompt-text {{
1510
+ background: #f3f4f6;
1511
+ border-radius: 6px;
1512
+ padding: 15px;
1513
+ font-family: monospace;
1514
+ white-space: pre-wrap;
1515
+ font-size: 14px;
1516
+ color: #374151;
1517
+ max-height: 400px;
1518
+ overflow-y: auto;
1519
+ }}
1520
+
1521
+ /* JSON 미리보기 스타일 */
1522
+ .json-details {{
1523
+ margin-top: 15px;
1524
+ }}
1525
+
1526
+ .json-details summary {{
1527
+ cursor: pointer;
1528
+ padding: 10px;
1529
+ background: #f0f0f0;
1530
+ border-radius: 5px;
1531
+ font-weight: 500;
1532
+ }}
1533
+
1534
+ .json-preview {{
1535
+ background: #f8f8f8;
1536
+ padding: 15px;
1537
+ border-radius: 5px;
1538
+ border: 1px solid #ddd;
1539
+ margin-top: 10px;
1540
+ overflow-x: auto;
1541
+ color: #333;
1542
+ font-family: monospace;
1543
+ font-size: 14px;
1544
+ line-height: 1.5;
1545
+ max-height: 400px;
1546
+ overflow-y: auto;
1547
+ }}
1548
+
1549
+ .error {{
1550
+ color: #e53e3e;
1551
+ padding: 10px;
1552
+ background: #fff5f5;
1553
+ border-radius: 5px;
1554
+ margin-top: 10px;
1555
+ }}
1556
+ </style>
1557
+
1558
+ <div class="backend-persona">
1559
+ <div class="backend-header">
1560
+ <h2>{name} - 백엔드 데이터</h2>
1561
+ <p>상세 정보와 내부 변수 확인</p>
1562
+ </div>
1563
+
1564
+ <div class="backend-section">
1565
+ <h3 class="section-title">기본 정보</h3>
1566
+ <table class="basic-info-table">
1567
+ {basic_info_html}
1568
+ </table>
1569
+ </div>
1570
+
1571
+ <div class="backend-section">
1572
+ <h3 class="section-title">성격 요약 (Big 5)</h3>
1573
+ {big5_html}
1574
+ </div>
1575
+
1576
+ <div class="backend-section">
1577
+ <h3 class="section-title">유머 매트릭스 (3차원)</h3>
1578
+ {humor_html}
1579
+ </div>
1580
+
1581
+ <div class="backend-section">
1582
+ <h3 class="section-title">매력적 결함</h3>
1583
+ {flaws_html}
1584
+
1585
+ <h3 class="section-title" style="margin-top: 20px;">모순적 특성</h3>
1586
+ {contradictions_html}
1587
+ </div>
1588
+
1589
+ {prompt_html}
1590
+
1591
+ <div class="backend-section">
1592
+ <h3 class="section-title">전체 백엔드 데이터</h3>
1593
+ {json_preview}
1594
+ </div>
1595
+ </div>
1596
+ """
1597
+
1598
+ return html
1599
+
1600
+ def get_personas_list():
1601
+ """Get list of personas for the dataframe"""
1602
+ personas = list_personas()
1603
+
1604
+ # Convert to dataframe format
1605
+ df_data = []
1606
+ for i, persona in enumerate(personas):
1607
+ df_data.append([
1608
+ persona["name"],
1609
+ persona["type"],
1610
+ persona["created_at"],
1611
+ persona["filename"]
1612
+ ])
1613
+
1614
+ return df_data, personas
1615
+
1616
+ def load_selected_persona(selected_row, personas_list):
1617
+ """Load persona from the selected row in the dataframe"""
1618
+ if selected_row is None or len(selected_row) == 0:
1619
+ return None, "선택된 페르소나가 없습니다.", None, None, None
1620
+
1621
+ try:
1622
+ # Get filepath from selected row
1623
+ selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0
1624
+ filepath = personas_list[selected_index]["filepath"]
1625
+
1626
+ # Load persona
1627
+ persona = load_persona(filepath)
1628
+ if not persona:
1629
+ return None, "페르소나 로딩에 실패했습니다.", None, None, None
1630
+
1631
+ # Generate HTML views
1632
+ frontend_view, backend_view = toggle_frontend_backend_view(persona)
1633
+ frontend_html = create_frontend_view_html(frontend_view)
1634
+ backend_html = create_backend_view_html(backend_view)
1635
+
1636
+ # Generate personality chart
1637
+ chart_image_path = generate_personality_chart(frontend_view)
1638
+
1639
+ return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image_path
1640
+
1641
+ except Exception as e:
1642
+ return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None
1643
+
1644
+ # 페르소나와 대화하는 함수 추가
1645
+ def chat_with_persona(persona, user_message, chat_history=None):
1646
+ """
1647
+ 페르소나와 대화하는 함수
1648
+ """
1649
+ if chat_history is None:
1650
+ chat_history = []
1651
+
1652
+ if not user_message.strip():
1653
+ return chat_history, ""
1654
+
1655
+ if not persona:
1656
+ # Gradio 4.x 호환 메시지 형식 (튜플)
1657
+ chat_history.append([user_message, "페르소나가 로드되지 않았습니다. 먼저 페르소나를 생성하거나 불러오세요."])
1658
+ return chat_history, ""
1659
+
1660
+ try:
1661
+ # 페르소나 생성기에서 대화 기능 호출
1662
+ # 이전 대화 기록 변환 필요 - 리스트에서 튜플 형식으로
1663
+ converted_history = []
1664
+ for msg in chat_history:
1665
+ if isinstance(msg, list) and len(msg) == 2:
1666
+ # 리스트 형식이면 튜플로 변환
1667
+ converted_history.append((msg[0] if msg[0] else "", msg[1] if msg[1] else ""))
1668
+ elif isinstance(msg, tuple) and len(msg) == 2:
1669
+ # 이미 튜플 형식이면 그대로 사용
1670
+ converted_history.append(msg)
1671
+
1672
+ # 페르소나 생성기에서 대화 함수 호출
1673
+ response = persona_generator.chat_with_persona(persona, user_message, converted_history)
1674
+
1675
+ # Gradio 4.x 메시지 형식으로 추가 (리스트)
1676
+ chat_history.append([user_message, response])
1677
+
1678
+ return chat_history, ""
1679
+ except Exception as e:
1680
+ import traceback
1681
+ error_details = traceback.format_exc()
1682
+ print(f"대화 오류: {error_details}")
1683
+ chat_history.append([user_message, f"대화 중 오류가 발생했습니다: {str(e)}"])
1684
+ return chat_history, ""
1685
+
1686
+ # 메인 Gradio 인터페이스 구성 함수
1687
+ def create_interface():
1688
+ # 현재 persona 상태 저장 - Gradio 5.x에서 변경된 방식 적용
1689
+ current_persona = gr.State(value=None)
1690
+ personas_list = gr.State(value=[])
1691
+
1692
+ with gr.Blocks(theme=theme, css=css) as app:
1693
+ gr.Markdown("""
1694
+ # 놈팽쓰(MemoryTag): 당신 곁의 사물, 이제 친구가 되다
1695
+ 이 데모는 일상 속 사물에 AI 페르소나를 부여하여 대화할 수 있게 해주는 서비스입니다.
1696
+ """)
1697
+
1698
+ with gr.Tabs() as tabs:
1699
+ with gr.Tab("페르소나 생성", id="persona_creation"):
1700
+ with gr.Row():
1701
+ with gr.Column(scale=1):
1702
+ # 이미지 업로드 영역
1703
+ image_input = gr.Image(
1704
+ type="pil",
1705
+ width=300,
1706
+ height=300,
1707
+ label="사물 이미지를 업로드하세요"
1708
+ )
1709
+ # 입력 필드들
1710
+ with gr.Group():
1711
+ gr.Markdown("### 맥락 정보 입력")
1712
+ name_input = gr.Textbox(label="사물 이름 (빈칸일 경우 자동 생성)", placeholder="예: 책상 위 램프")
1713
+
1714
+ location_input = gr.Dropdown(
1715
+ choices=["집", "사무실", "여행 중", "상점", "학교", "카페", "기타"],
1716
+ label="주로 어디에 있나요?",
1717
+ value="집"
1718
+ )
1719
+
1720
+ time_spent_input = gr.Dropdown(
1721
+ choices=["새것", "몇 개월", "1년 이상", "오래됨", "중고/빈티지"],
1722
+ label="얼마나 함께했나요?",
1723
+ value="몇 개월"
1724
+ )
1725
+
1726
+ object_type_input = gr.Dropdown(
1727
+ choices=["가전제품", "가구", "전자기기", "장식품", "도구", "개인용품", "기타"],
1728
+ label="어떤 종류의 사물인가요?",
1729
+ value="가구"
1730
+ )
1731
+
1732
+ # 사용자 입력들 상태 저장 - Gradio 5.x에서 변경된 방식 적용
1733
+ user_inputs = gr.State(value={})
1734
+
1735
+ with gr.Row():
1736
+ discover_btn = gr.Button("1. 영혼 발견하기", variant="primary")
1737
+ create_btn = gr.Button("2. 페르소나 생성", variant="secondary")
1738
+
1739
+ # 영혼 깨우기 결과 표시 영역
1740
+ awakening_output = gr.HTML(visible=False)
1741
+ error_output = gr.Markdown(visible=False)
1742
+
1743
+ with gr.Column(scale=1):
1744
+ # 이미지 분석 결과
1745
+ image_analysis_output = gr.JSON(label="이미지 분석 결과", visible=False)
1746
+ # 페르소나 기본 정보 및 특성
1747
+ basic_info_output = gr.JSON(label="기본 정보")
1748
+ personality_traits_output = gr.JSON(label="페르소나 특성")
1749
+
1750
+ # 페르소나 저장 및 내보내기 버튼
1751
+ with gr.Row():
1752
+ save_btn = gr.Button("페르소나 저장", variant="primary")
1753
+ download_btn = gr.Button("JSON으로 내보내기", variant="secondary")
1754
+
1755
+ # 성향 미세조정
1756
+ with gr.Accordion("성향 미세조정", open=False):
1757
+ with gr.Row():
1758
+ with gr.Column(scale=1):
1759
+ warmth_slider = gr.Slider(0, 100, label="온기", step=1)
1760
+ competence_slider = gr.Slider(0, 100, label="능력", step=1)
1761
+ creativity_slider = gr.Slider(0, 100, label="창의성", step=1)
1762
+ with gr.Column(scale=1):
1763
+ extraversion_slider = gr.Slider(0, 100, label="외향성", step=1)
1764
+ humor_slider = gr.Slider(0, 100, label="유머감각", step=1)
1765
+ trust_slider = gr.Slider(0, 100, label="신뢰도", step=1)
1766
+
1767
+ humor_style = gr.Dropdown(
1768
+ choices=["witty_wordsmith", "warm_humorist", "playful_trickster", "sharp_observer", "self_deprecating"],
1769
+ label="유머 스타일",
1770
+ value="warm_humorist"
1771
+ )
1772
+ apply_traits_btn = gr.Button("성향 적용하기")
1773
+
1774
+ # 유머 스타일 시각화
1775
+ humor_chart_output = gr.Plot(label="유머 스타일 매트릭스")
1776
+
1777
+ # 페르소나 다운로드 관련 출력
1778
+ json_output = gr.Textbox(label="JSON 데이터", visible=False)
1779
+ download_output = gr.File(label="다운로드", visible=False)
1780
+
1781
+ with gr.Tab("세부 정보", id="persona_details"):
1782
+ with gr.Row():
1783
+ with gr.Column(scale=1):
1784
+ # 매력적 결함 데이터프레임
1785
+ attractive_flaws_df_output = gr.Dataframe(
1786
+ headers=["매력적 결함", "효과"],
1787
+ label="매력적 결함",
1788
+ interactive=False
1789
+ )
1790
+
1791
+ # 모순적 특성 데이터프레임
1792
+ contradictions_df_output = gr.Dataframe(
1793
+ headers=["모순적 특성", "효과"],
1794
+ label="모순적 특성",
1795
+ interactive=False
1796
+ )
1797
+
1798
+ with gr.Column(scale=1):
1799
+ # 성격 차트
1800
+ personality_chart_output = gr.Plot(label="성격 차트")
1801
+
1802
+ # 127개 성격 변수 데이터프레임
1803
+ with gr.Accordion("127개 성격 변수 세부정보", open=False):
1804
+ personality_variables_df_output = gr.Dataframe(
1805
+ headers=["변수", "값", "설명"],
1806
+ label="성격 변수 (127개)",
1807
+ interactive=False
1808
+ )
1809
+
1810
+ with gr.Tab("대화하기", id="persona_chat"):
1811
+ with gr.Row():
1812
+ with gr.Column(scale=1):
1813
+ # 페르소나 불러오기 기능
1814
+ gr.Markdown("### 페르소나 불러오기")
1815
+
1816
+ with gr.Row():
1817
+ with gr.Column(scale=1):
1818
+ # 저장된 페르소나 목록
1819
+ refresh_personas_btn = gr.Button("목록 새로고침", variant="secondary")
1820
+ persona_table = gr.Dataframe(
1821
+ headers=["ID", "이름", "유형", "생성 날짜"],
1822
+ label="저장된 페르소나",
1823
+ interactive=False
1824
+ )
1825
+ load_persona_btn = gr.Button("선택한 페르소나 불러오기", variant="primary")
1826
+
1827
+ with gr.Column(scale=1):
1828
+ # JSON 파일에서 불러오기
1829
+ gr.Markdown("### 또는 JSON 파일에서 불러오기")
1830
+ json_upload = gr.File(
1831
+ label="페르소나 JSON 파일 업로드",
1832
+ file_types=[".json"]
1833
+ )
1834
+ import_persona_btn = gr.Button("JSON에서 가져오기", variant="primary")
1835
+ import_status = gr.Markdown("")
1836
+
1837
+ with gr.Column(scale=1):
1838
+ # 현재 로드된 페르소나 정보
1839
+ chat_persona_info = gr.Markdown("### 페르소나를 불러와 대화를 시작하세요")
1840
+
1841
+ # 대화 인터페이스
1842
+ chatbot = gr.Chatbot(height=400, label="대화")
1843
+ with gr.Row():
1844
+ message_input = gr.Textbox(
1845
+ placeholder="메시지를 입력하세요...",
1846
+ label="메시지",
1847
+ show_label=False,
1848
+ lines=2
1849
+ )
1850
+ send_btn = gr.Button("전송", variant="primary")
1851
+
1852
+ # 영혼 깨우기 버튼 이벤트
1853
+ discover_btn.click(
1854
+ fn=lambda name, location, time_spent, object_type: {"name": name, "location": location, "time_spent": time_spent, "object_type": object_type},
1855
+ inputs=[name_input, location_input, time_spent_input, object_type_input],
1856
+ outputs=[user_inputs],
1857
+ queue=False
1858
+ ).then(
1859
+ fn=show_awakening_progress,
1860
+ inputs=[image_input, user_inputs],
1861
+ outputs=[current_persona, error_output, awakening_output],
1862
+ queue=True
1863
+ )
1864
+
1865
+ # 페르소나 생성 버튼 이벤트
1866
+ create_btn.click(
1867
+ fn=lambda name, location, time_spent, object_type: {"name": name, "location": location, "time_spent": time_spent, "object_type": object_type},
1868
+ inputs=[name_input, location_input, time_spent_input, object_type_input],
1869
+ outputs=[user_inputs],
1870
+ queue=False
1871
+ ).then(
1872
+ fn=create_persona_from_image,
1873
+ inputs=[image_input, user_inputs],
1874
+ outputs=[
1875
+ current_persona, error_output, image_input, image_analysis_output,
1876
+ basic_info_output, personality_traits_output, humor_chart_output,
1877
+ attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output
1878
+ ],
1879
+ queue=True
1880
+ ).then(
1881
+ fn=generate_personality_chart,
1882
+ inputs=[current_persona],
1883
+ outputs=[personality_chart_output]
1884
+ ).then(
1885
+ fn=lambda: gr.update(visible=False),
1886
+ outputs=[awakening_output]
1887
+ ).then(
1888
+ fn=lambda persona: [
1889
+ 50, 50, 50, 50, 50, 50 # 기본값
1890
+ ],
1891
+ inputs=[current_persona],
1892
+ outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider]
1893
+ )
1894
+
1895
+ # 성향 미세조정 이벤트
1896
+ apply_traits_btn.click(
1897
+ fn=refine_persona,
1898
+ inputs=[
1899
+ current_persona, warmth_slider, competence_slider, creativity_slider,
1900
+ extraversion_slider, humor_slider, trust_slider, humor_style
1901
+ ],
1902
+ outputs=[
1903
+ current_persona, basic_info_output, personality_traits_output,
1904
+ humor_chart_output, personality_chart_output, personality_variables_df_output
1905
+ ]
1906
+ )
1907
+
1908
+ # 페르소나 저장 버튼 이벤트
1909
+ save_btn.click(
1910
+ fn=save_current_persona,
1911
+ inputs=[current_persona],
1912
+ outputs=[error_output]
1913
+ )
1914
+
1915
+ # 페르소나 JSON 내보내기 버튼 이벤트
1916
+ download_btn.click(
1917
+ fn=export_persona_json,
1918
+ inputs=[current_persona],
1919
+ outputs=[download_output, json_output]
1920
+ ).then(
1921
+ fn=lambda x: gr.update(visible=True if x else False),
1922
+ inputs=[download_output],
1923
+ outputs=[download_output]
1924
+ ).then(
1925
+ fn=lambda x: gr.update(visible=False),
1926
+ inputs=[json_output],
1927
+ outputs=[json_output]
1928
+ )
1929
+
1930
+ # 저장된 페르소나 목록 새로고침 이벤트
1931
+ refresh_personas_btn.click(
1932
+ fn=get_personas_list,
1933
+ outputs=[persona_table, personas_list]
1934
+ )
1935
+
1936
+ # 저장된 페르소나 불러오기 이벤트
1937
+ load_persona_btn.click(
1938
+ fn=load_selected_persona,
1939
+ inputs=[persona_table, personas_list],
1940
+ outputs=[
1941
+ current_persona, chat_persona_info, chatbot,
1942
+ basic_info_output, personality_traits_output, humor_chart_output,
1943
+ attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output
1944
+ ]
1945
+ ).then(
1946
+ fn=generate_personality_chart,
1947
+ inputs=[current_persona],
1948
+ outputs=[personality_chart_output]
1949
+ ).then(
1950
+ fn=lambda persona: [50, 50, 50, 50, 50, 50], # 기본값
1951
+ inputs=[current_persona],
1952
+ outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider]
1953
+ ).then(
1954
+ fn=lambda: gr.update(selected="persona_creation"),
1955
+ outputs=[tabs]
1956
+ )
1957
+
1958
+ # JSON에서 페르소나 가져오기 이벤트
1959
+ import_persona_btn.click(
1960
+ fn=import_persona_json,
1961
+ inputs=[json_upload],
1962
+ outputs=[current_persona, import_status]
1963
+ ).then(
1964
+ fn=lambda persona: update_current_persona_info(persona) if persona else (None, None, None, [], [], []),
1965
+ inputs=[current_persona],
1966
+ outputs=[
1967
+ basic_info_output, personality_traits_output, humor_chart_output,
1968
+ attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output
1969
+ ]
1970
+ ).then(
1971
+ fn=generate_personality_chart,
1972
+ inputs=[current_persona],
1973
+ outputs=[personality_chart_output]
1974
+ ).then(
1975
+ fn=lambda persona: [50, 50, 50, 50, 50, 50], # 기본값
1976
+ inputs=[current_persona],
1977
+ outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider]
1978
+ ).then(
1979
+ fn=lambda persona: f"### 페르소나를 불러왔습니다" if persona else "### 페르소나를 불러오지 못했습니다",
1980
+ inputs=[current_persona],
1981
+ outputs=[chat_persona_info]
1982
+ ).then(
1983
+ fn=lambda: gr.update(selected="persona_creation"),
1984
+ outputs=[tabs]
1985
+ )
1986
+
1987
+ # 메시지 전송 이벤트
1988
+ send_btn.click(
1989
+ fn=chat_with_persona,
1990
+ inputs=[current_persona, message_input, chatbot],
1991
+ outputs=[chatbot, message_input]
1992
+ )
1993
+ message_input.submit(
1994
+ fn=chat_with_persona,
1995
+ inputs=[current_persona, message_input, chatbot],
1996
+ outputs=[chatbot, message_input]
1997
+ )
1998
+
1999
+ # 앱 로드 시 저장된 페르소나 목록 로드
2000
+ app.load(
2001
+ fn=get_personas_list,
2002
+ outputs=[persona_table, personas_list]
2003
+ )
2004
+
2005
+ return app
2006
+
2007
+ # 메인 실행 부분
2008
+ if __name__ == "__main__":
2009
+ app = create_interface()
2010
+ app.launch(server_name="0.0.0.0", server_port=7860)
info.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 놈팽쓰(MemoryTag) Hugging Face 데모 가이드
2
+
3
+ ## 🎯 핵심 컨셉
4
+ "새로운 것을 만드는 게 아니라, 이미 그곳에 있던 영혼을 깨우는 것"
5
+
6
+ 사물의 AI 페르소나를 생성하는 기술적 측면보다는, 사물에 내재된 고유한 영혼이 깨어나는 경험을 제공합니다.
7
+
8
+ ## 🧭 앱 이용 프로세스
9
+
10
+ ```mermaid
11
+ flowchart LR
12
+ A[1. 이미지 업로드 & 분석] --> B[2. 추가 정보 입력]
13
+ B --> C[3. 페르소나 생성 & 미세조정]
14
+ C --> D[4. 페르소나 저장]
15
+
16
+ style A fill:#e1f5fe
17
+ style B fill:#f3e5f5
18
+ style C fill:#e8f5e8
19
+ style D fill:#fff3e0
20
+ ```
21
+
22
+ ### 1차 단계: 이미지 업로드 및 기본 분석
23
+ - **액션**: 사물 이미지 업로드 → "영혼 발견하기" 버튼 클릭
24
+ - **결과**: 영혼 깨어나는 과정 애니메이션 표시 (백엔드 분석 과정 시각화)
25
+ - **백엔드 과정**:
26
+ * 이미지 특성 분석 (색상, 형태, 재질 등)
27
+ * 127개 성격 변수 초기화
28
+ * 기본 페르소나 틀 생성
29
+
30
+ ### 2차 단계: 맥락 정보 입력
31
+ - **액션**: 추가 정보 입력 → "페르소나 생성" 버튼 클릭
32
+ * 사물 이름 (자동 생성 가능)
33
+ * 위치 (집, 사무실, 여행 중 등)
34
+ * 함께한 시간 (새것, 몇 개월, 오래됨 등)
35
+ * 사물 종류 (가구, 전자기기 등)
36
+ - **결과**: 맥락이 반영된 페르소나 정보 표시
37
+ - **백엔드 과정**:
38
+ * 맥락 정보 기반 성격 특성 조정
39
+ * 127개 성격 변수 최종화
40
+ * 매력적 결함 및 모순적 특성 생성
41
+
42
+ ### 3차 단계: 페르소나 탐색 및 미세조정
43
+ - **액션**: 페르소나 탐색 및 미세조정
44
+ * 대화 미리보기 확인
45
+ * **사용자 조정 가능한 4개 핵심 지표 슬라이더**:
46
+ - 얼마나 말씀하세요? (내성적 ↔ 외향적)
47
+ - 감정을 잘 표현하나요? (담담함 ↔ 감정 풍부)
48
+ - 밝아 만족가요? (조용함 ↔ 에너지)
49
+ - 어떤 방식으로 문제를 풀까요? (논리적사고 ↔ 직관적사고)
50
+ * **유머 스타일 선택**: 4가지 옵션 중 선택
51
+ * **자동 생성**: 나머지 123개 성격 변수는 핵심 지표를 바탕으로 자동 계산
52
+ - **결과**: 조정 사항이 실시간으로 반영된 페르소나 정보
53
+ - **프론트엔드 뷰**:
54
+ * 성격 차트 (영어로 표시)
55
+ * 특성 카드
56
+ * 대화 샘플
57
+
58
+ ### 4차 단계: 페르소나 저장
59
+ - **액션**: "페르소나 저장" 버튼 클릭
60
+ - **결과**:
61
+ * 성공 메시지 표시
62
+ * JSON으로 내보내기 옵션
63
+ * 대화하기 탭으로 이동 제안
64
+
65
+ ## 📱 데모 화면 구성
66
+
67
+ ### 첫 화면 (영혼 발견하기)
68
+ ```
69
+ ┌─────────────────────────────────────────────┐
70
+ │ 놈팽쓰(MemoryTag): 당신 곁의 사물, 이제 친구가 되다 │
71
+ ├─────────────────────────────────────────────┤
72
+ │ │
73
+ │ 🔍 프로세스 안내: │
74
+ │ 1️⃣ 이미지 업로드 → 2️⃣ 맥락 입력 → │
75
+ │ 3️⃣ 페르소나 탐색 → 4️⃣ 저장 │
76
+ │ │
77
+ │ ┌───────────────────┐ ┌───────────────┐ │
78
+ │ │ │ │ 맥락 정보 입력 │ │
79
+ │ │ 이미지 업로드 │ │ ⃞ 이름: │ │
80
+ │ │ 영역 │ │ ⃞ 위치: │ │
81
+ │ │ │ │ ⃞ 함께한 시간: │ │
82
+ │ │ │ │ ⃞ 사물 종류: │ │
83
+ │ └───────────────────┘ └───────────────┘ │
84
+ │ │
85
+ │ [1. 영혼 발견하기] [2. 페르소나 생성] │
86
+ │ │
87
+ │ 아래에 영혼 깨어나는 과정이 표시됩니다 ↓ │
88
+ │ ┌───────────────────────────────────────┐ │
89
+ │ │ 영혼 깨어나는 과정 │ │
90
+ │ │ │ │
91
+ │ └───────────────────────────────────────┘ │
92
+ └─────────────────────────────────────────────┘
93
+ ```
94
+
95
+ ## 💻 개발자 참고사항
96
+
97
+ ### 핵심 함수 흐름
98
+ ```python
99
+ # 1차 단계: 이미지 업로드 및 기본 분석
100
+ show_awakening_progress(image, user_inputs) → 영혼 깨어나는 과정 시각화
101
+
102
+ # 2차 단계: 맥락 정보 입력 및 페르소나 생성
103
+ create_persona_from_image(image, user_inputs) → 페르소나 객체 생성
104
+
105
+ # 3차 단계: 페르소나 미세조정
106
+ refine_persona(persona, warmth, competence, ...) → 조정된 페르소나
107
+
108
+ # 4차 단계: 페르소나 저장
109
+ save_current_persona(current_persona) → 저장 결과
110
+ ```
111
+
112
+ ### 주요 모듈 역할
113
+ - **app.py**: 메인 인터페이스 및 이벤트 핸들러
114
+ - **modules/persona_generator.py**: 페르소나 생성 로직
115
+ - **modules/data_manager.py**: 데이터 저장/로드
116
+ - **temp/view_functions.py**: 차트 생성 및 페르소나 시각화
117
+
118
+ ## 📘 앱 활용 팁
119
+ 1. **이미지 선택**: 특징이 뚜렷한 사물 이미지가 더 흥미로운 페르소나를 생성합니다
120
+ 2. **맥락 활용**: 맥락 정보가 풍부할수록 페르소나가 더 독특해집니다
121
+ 3. **미세조정**: 성격 슬라이더로 페르소나의 특성을 원하는 방향으로 조정해보세요
122
+ 4. **대화 시작**: 간단한 인사로 시작해 점차 깊은 대화로 발전시켜보세요
123
+
124
+ 이 가이드는 놈팽쓰 앱의 핵심 경험을 간략하지만 효과적으로 체험할 수 있도록 구성되었습니다.
125
+
126
+ ### 핵심 설계 원칙
127
+ - **사용자 친화적**: 복잡한 127개 변수를 4개 핵심 지표로 단순화
128
+ - **전문성 유지**: 백엔드에서는 여전히 127개 변수로 정교한 페르소나 생성
129
+ - **실시간 반영**: 사용자 조정 시 즉시 페르소나 업데이트
130
+ - **직관적 질문**: "얼마나 말씀하세요?", "감정을 잘 표현하나요?" 등 이해하기 쉬운 질문
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio==4.44.1
2
  google-generativeai==0.3.2
3
  Pillow==10.3.0
4
  python-dotenv==1.0.0
 
1
+ gradio==5.31.0
2
  google-generativeai==0.3.2
3
  Pillow==10.3.0
4
  python-dotenv==1.0.0