minjune121 commited on
Commit
a4acd69
Β·
verified Β·
1 Parent(s): cc5301d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +431 -419
app.py CHANGED
@@ -1,495 +1,507 @@
 
 
 
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import numpy as np
4
  import torch
5
- from transformers import pipeline
6
- from sentence_transformers import SentenceTransformer, util
7
  import json
 
 
 
8
  from datetime import datetime
9
- import os
 
 
 
 
10
 
11
- # ===============================
12
  # μ„€μ •
13
- # ===============================
 
 
 
 
 
 
14
  device = 0 if torch.cuda.is_available() else -1
15
- FEEDBACK_FILE = "feedback_data.jsonl"
16
- BOOK_DB_FILE = "book_db_final.csv"
17
 
18
- # ===============================
19
- # λͺ¨λΈ λ‘œλ“œ
20
- # ===============================
21
  print("πŸš€ λͺ¨λΈ λ‘œλ”© 쀑...")
22
 
23
  try:
24
- stt_model = pipeline(
25
  "automatic-speech-recognition",
26
  model="openai/whisper-large-v3-turbo",
27
- device=device
28
  )
29
  print("βœ… STT λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
30
  except Exception as e:
31
- print(f"⚠️ STT λͺ¨λΈ λ‘œλ“œ μ‹€νŒ¨: {e}")
32
  stt_model = None
33
 
34
  try:
35
- emotion_model = pipeline(
36
- "text-classification",
37
- model="monologg/koelectra-base-v3-goemotions",
38
- device=device,
39
- top_k=None
40
- )
41
- print("βœ… 감정 뢄석 λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
42
  except Exception as e:
43
- print(f"⚠️ 감정 뢄석 λͺ¨λΈ λ‘œλ“œ μ‹€νŒ¨: {e}")
44
- emotion_model = None
45
 
46
  try:
47
- sbert_model = SentenceTransformer("jhgan/ko-sroberta-multitask")
48
- print("βœ… μž„λ² λ”© λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
 
 
 
 
49
  except Exception as e:
50
- print(f"⚠️ μž„λ² λ”© λͺ¨λΈ λ‘œλ“œ μ‹€νŒ¨: {e}")
51
- sbert_model = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- print("βœ… λͺ¨λ“  λͺ¨λΈ λ‘œλ”© μ™„λ£Œ!")
54
 
55
- # ===============================
56
- # ��이터 λ‘œλ“œ
57
- # ===============================
58
  def load_book_data():
59
- """μ±… 데이터 λ‘œλ“œ 및 μž„λ² λ”© 생성"""
60
- if not os.path.exists(BOOK_DB_FILE):
61
- raise FileNotFoundError(f"❌ {BOOK_DB_FILE} 파일이 ν•„μš”ν•©λ‹ˆλ‹€.")
62
-
63
- df = pd.read_csv(BOOK_DB_FILE)
64
- print(f"πŸ“š {len(df)}개의 μ±… 데이터 λ‘œλ“œ μ™„λ£Œ")
65
-
66
- # ν•„μˆ˜ 컬럼 확인
67
- required_cols = ["title", "emotion"]
68
- if not all(col in df.columns for col in required_cols):
69
- raise ValueError(f"❌ ν•„μˆ˜ 컬럼 λˆ„λ½: {required_cols}")
70
-
71
- # ν…μŠ€νŠΈ 데이터 μ€€λΉ„
72
- book_texts = df["contents"].fillna(df["title"]).tolist()
73
-
74
- # μž„λ² λ”© 생성
75
- print("πŸ”„ μ±… μž„λ² λ”© 생성 쀑...")
76
- book_embeddings = sbert_model.encode(
77
- book_texts,
78
- convert_to_tensor=True,
79
- show_progress_bar=True
80
- )
81
- print("βœ… μž„λ² λ”© 생성 μ™„λ£Œ")
82
-
 
 
 
 
 
 
 
 
 
 
 
 
83
  return df, book_embeddings
84
 
85
  df, book_embeddings = load_book_data()
86
 
87
- # ===============================
88
- # 감정 λ§€ν•‘
89
- # ===============================
90
- EMOTION_MAP = {
91
- "joy": "기쁨",
92
- "sadness": "μŠ¬ν””",
93
- "anger": "λΆ„λ…Έ",
94
- "fear": "곡포",
95
- "surprise": "λ†€λžŒ",
96
- "disgust": "혐였",
97
- "love": "μ‹ λ’°",
98
- "optimism": "κΈ°λŒ€"
99
- }
100
 
101
- EMOTION_LABELS = ["기쁨", "μ‹ λ’°", "곡포", "λ†€λžŒ", "μŠ¬ν””", "혐였", "λΆ„λ…Έ", "κΈ°λŒ€"]
102
-
103
- # ===============================
104
- # ν”Όλ“œλ°± 데이터 관리
105
- # ===============================
106
- def save_feedback(user_text, emotion, books, feedback_type, book_title=None):
107
- """ν”Όλ“œλ°± 데이터λ₯Ό JSONL ν˜•μ‹μœΌλ‘œ μ €μž₯"""
108
- feedback_entry = {
109
- "timestamp": datetime.now().isoformat(),
110
- "user_text": user_text,
111
- "detected_emotion": emotion,
112
- "recommended_books": [b["title"] for b in books],
113
- "feedback_type": feedback_type,
114
- "selected_book": book_title
115
- }
116
-
117
  try:
118
- with open(FEEDBACK_FILE, "a", encoding="utf-8") as f:
119
- f.write(json.dumps(feedback_entry, ensure_ascii=False) + "\n")
 
 
 
 
 
 
 
120
  except Exception as e:
121
- print(f"⚠️ ν”Όλ“œλ°± μ €μž₯ μ‹€νŒ¨: {e}")
 
122
 
123
- def load_feedback_data():
124
- """μ €μž₯된 ν”Όλ“œλ°± 데이터 λ‘œλ“œ"""
125
- if not os.path.exists(FEEDBACK_FILE):
126
- return []
127
-
128
- feedback_data = []
129
  try:
130
- with open(FEEDBACK_FILE, "r", encoding="utf-8") as f:
131
- for line in f:
132
- try:
133
- feedback_data.append(json.loads(line.strip()))
134
- except json.JSONDecodeError:
135
- continue
136
- except Exception as e:
137
- print(f"⚠️ ν”Όλ“œλ°± λ‘œλ“œ μ‹€νŒ¨: {e}")
138
-
139
- return feedback_data
140
-
141
- def apply_feedback_learning():
142
- """ν”Όλ“œλ°± 데이터 기반 μΆ”μ²œ κ°€μ€‘μΉ˜ μ‘°μ •"""
143
- feedback_data = load_feedback_data()
144
-
145
- if not feedback_data:
146
- return {}
147
-
148
- weights = {}
149
-
150
- for entry in feedback_data:
151
- emotion = entry.get("detected_emotion")
152
- selected_book = entry.get("selected_book")
153
- feedback_type = entry.get("feedback_type")
154
-
155
- if emotion and selected_book:
156
- key = (emotion, selected_book)
157
-
158
- if key not in weights:
159
- weights[key] = 0
160
-
161
- if feedback_type in ["like", "select"]:
162
- weights[key] += 1.0
163
- elif feedback_type == "dislike":
164
- weights[key] -= 0.5
165
-
166
- return weights
167
-
168
- # ===============================
169
  # 감정 뢄석
170
- # ===============================
171
- def get_emotion_scores(text):
172
- """ν…μŠ€νŠΈμ—μ„œ 감정 점수 μΆ”μΆœ"""
173
- if not text or len(text.strip()) == 0:
174
- return {emo: 0.0 for emo in EMOTION_LABELS}
175
-
176
- if emotion_model is None:
177
- return {emo: 0.125 for emo in EMOTION_LABELS}
178
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  try:
180
- results = emotion_model(text)[0]
181
-
182
- scores = {emo: 0.0 for emo in EMOTION_LABELS}
183
-
184
- for r in results:
185
- label = r["label"].lower()
186
- mapped = EMOTION_MAP.get(label)
187
-
188
  if mapped:
189
- scores[mapped] += r["score"]
190
-
191
- # ν•œκ΅­μ–΄ ν‚€μ›Œλ“œ 기반 보정
192
- text_lower = text.lower()
193
-
194
- keyword_boosts = {
195
- "μŠ¬ν””": ["μŠ¬ν”„", "우울", "눈물", "νž˜λ“€", "μ™Έλ‘œ"],
196
- "λΆ„λ…Έ": ["ν™”λ‚˜", "짜증", "μ—΄λ°›", "빑치", "μ–΅μšΈ"],
197
- "기쁨": ["행볡", "μ’‹λ‹€", "기쁘", "즐겁", "μ‹ λ‚˜"],
198
- "곡포": ["무섭", "두렡", "κ±±μ •", "λΆˆμ•ˆ"],
199
- "λ†€λžŒ": ["λ†€λž", "깜짝", "좩격"],
200
- "μ‹ λ’°": ["믿음", "μ‚¬λž‘", "λ”°λœ»", "고마"],
201
- "κΈ°λŒ€": ["κΈ°λŒ€", "희망", "μ„€λ ˆ"]
202
- }
203
-
204
- for emotion, keywords in keyword_boosts.items():
205
- for keyword in keywords:
206
- if keyword in text_lower:
207
- scores[emotion] += 0.3
208
- break
209
-
210
- # μ •κ·œν™”
211
- total = sum(scores.values())
212
- if total > 0:
213
- scores = {k: v / total for k, v in scores.items()}
214
-
215
- return scores
216
  except Exception as e:
217
- print(f"⚠️ 감정 뢄석 였λ₯˜: {e}")
218
- return {emo: 0.125 for emo in EMOTION_LABELS}
219
-
220
- # ===============================
221
- # μΆ”μ²œ μ‹œμŠ€ν…œ
222
- # ===============================
223
- def recommend_books(user_text, emotion, top_n=3):
224
- """감정에 λ§žλŠ” μ±… μΆ”μ²œ (ν”Όλ“œλ°± ν•™μŠ΅ 반영)"""
225
-
226
- if sbert_model is None:
 
 
 
 
 
 
 
227
  return []
228
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  try:
230
- pool = df[df["emotion"] == emotion].copy()
231
-
232
- if pool.empty:
233
- pool = df.copy()
234
-
235
- idxs = pool.index.tolist()
236
- pool_embs = book_embeddings[idxs]
237
-
238
- user_emb = sbert_model.encode(user_text, convert_to_tensor=True)
239
-
240
- sims = util.cos_sim(user_emb, pool_embs)[0].cpu().numpy()
241
-
242
- pool["sim"] = sims
243
-
244
- feedback_weights = apply_feedback_learning()
245
-
246
- def calculate_final_score(row):
247
- base_score = row["sim"]
248
- key = (emotion, row["title"])
249
- feedback_boost = feedback_weights.get(key, 0) * 0.1
250
- return base_score + feedback_boost
251
-
252
- pool["final_score"] = pool.apply(calculate_final_score, axis=1)
253
- pool = pool.sort_values("final_score", ascending=False).head(top_n)
254
-
255
- books = []
256
- for _, row in pool.iterrows():
257
- books.append({
258
- "title": row["title"],
259
- "img_url": row.get("thumbnail", ""),
260
- "content": str(row.get("contents", ""))[:150] + "...",
261
- "similarity": round(float(row["sim"]), 3),
262
- "final_score": round(float(row["final_score"]), 3)
263
- })
264
-
265
- return books
266
  except Exception as e:
267
- print(f"⚠️ μΆ”μ²œ 였λ₯˜: {e}")
268
- return []
269
 
270
- # ===============================
271
  # 메인 처리 ν•¨μˆ˜
272
- # ===============================
273
  def process_voice(audio_input):
274
- """μŒμ„± μž…λ ₯ 처리 및 μ±… μΆ”μ²œ"""
275
-
276
- empty_result = {
277
- "status": "error",
278
- "user_input": "",
279
- "emotion_label": "",
280
- "emotion_score": 0.0,
281
- "books": [],
282
- "message": ""
283
- }
284
-
285
  if audio_input is None:
286
- empty_result["message"] = "🎀 μŒμ„±μ„ λ…ΉμŒν•΄μ£Όμ„Έμš”."
287
- return empty_result
288
-
289
  if stt_model is None:
290
- empty_result["message"] = "❌ STT λͺ¨λΈμ΄ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
291
- return empty_result
292
-
293
  try:
294
  sr, y = audio_input
295
- y = y.astype(np.float32)
296
-
297
- max_val = np.max(np.abs(y))
298
- if max_val > 0:
299
- y = y / max_val
300
-
301
  stt_result = stt_model({"sampling_rate": sr, "raw": y})
302
- final_text = stt_result["text"].strip()
303
-
304
- if not final_text:
305
- empty_result["message"] = "❌ μŒμ„±μ΄ μΈμ‹λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
306
- return empty_result
307
-
308
- scores = get_emotion_scores(final_text)
309
- top_emotions = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:3]
310
- best_emotion, best_score = top_emotions[0]
311
-
312
- books = recommend_books(final_text, best_emotion, top_n=3)
313
-
314
- result = {
315
- "status": "success",
316
- "user_input": final_text,
317
- "emotion_label": best_emotion,
318
- "emotion_score": round(best_score, 3),
319
- "books": books,
320
- "message": f"✨ '{best_emotion}' 감정에 λ§žλŠ” 책을 μΆ”μ²œν•΄λ“œλ¦½λ‹ˆλ‹€."
321
- }
322
-
323
- save_feedback(final_text, best_emotion, books, "recommend")
324
-
325
- return result
326
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  except Exception as e:
328
- empty_result["message"] = f"❌ 였λ₯˜ λ°œμƒ: {str(e)}"
329
- print(f"처리 였λ₯˜: {e}")
330
- return empty_result
331
-
332
- def record_book_selection(user_text, emotion, books, selected_title):
333
- """μ‚¬μš©μžκ°€ 책을 μ„ νƒν–ˆμ„ λ•Œ ν”Όλ“œλ°± μ €μž₯"""
334
- if selected_title and user_text:
335
- save_feedback(user_text, emotion, books, "select", selected_title)
336
- return f"βœ… '{selected_title}' 선택이 κΈ°λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ν•™μŠ΅μ— λ°˜μ˜λ©λ‹ˆλ‹€!"
337
- return "⚠️ 선택 기둝 μ‹€νŒ¨"
338
-
339
- # ===============================
340
  # Gradio UI
341
- # ===============================
342
  custom_css = """
343
- .feedback-section {
344
- background-color: #f0f8ff;
345
- padding: 20px;
346
- border-radius: 10px;
347
- }
348
  """
349
 
350
- with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
351
-
352
- gr.Markdown(
353
- """
354
- # πŸ“š Boolook: μŒμ„± 기반 감정 뢄석 μ±… μΆ”μ²œ
355
-
356
- λ‹Ήμ‹ μ˜ 감정을 말둜 ν‘œν˜„ν•˜λ©΄, AIκ°€ λΆ„μ„ν•˜μ—¬ λ”± λ§žλŠ” 책을 μΆ”μ²œν•΄λ“œλ¦½λ‹ˆλ‹€.
357
-
358
- 🎀 **μ‚¬μš© 방법:**
359
- 1. 마이크 λ²„νŠΌμ„ 눌러 ν˜„μž¬ 감정을 ν‘œν˜„ν•΄λ³΄μ„Έμš”
360
- 2. "λΆ„μ„ν•˜κΈ°" λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”
361
- 3. μΆ”μ²œλ°›μ€ μ±… 쀑 λ§ˆμŒμ— λ“œλŠ” 책을 μ„ νƒν•΄μ£Όμ„Έμš” (선택 μ‹œ ν•™μŠ΅λ©λ‹ˆλ‹€!)
362
- """
363
- )
364
-
365
- state_text = gr.State("")
366
  state_emotion = gr.State("")
367
- state_books = gr.State([])
368
-
369
  with gr.Row():
370
  with gr.Column(scale=1):
371
  gr.Markdown("### 🎀 μŒμ„± μž…λ ₯")
372
  audio_in = gr.Audio(
373
  label="마이크둜 감정 ν‘œν˜„ν•˜κΈ°",
374
  sources=["microphone"],
375
- type="numpy"
376
- )
377
-
378
- analyze_btn = gr.Button(
379
- "πŸ” λΆ„μ„ν•˜κΈ°",
380
- variant="primary",
381
- size="lg"
382
- )
383
-
384
- gr.Markdown(
385
- """
386
- **πŸ’‘ 팁:**
387
- - "였늘 λ„ˆλ¬΄ μŠ¬νΌμš”"
388
- - "ν–‰λ³΅ν•œ κΈ°λΆ„μ΄μ—μš”"
389
- - "ν™”κ°€ λ‚˜λŠ” 일이 μžˆμ—ˆμ–΄μš”"
390
- """
391
  )
392
-
 
 
 
 
 
 
 
393
  with gr.Column(scale=1):
394
  gr.Markdown("### πŸ“Š 뢄석 κ²°κ³Ό")
395
-
396
- output_json = gr.JSON(
397
- label="상세 κ²°κ³Ό",
398
- visible=True
399
- )
400
-
401
- with gr.Row():
402
- gr.Markdown("### πŸ“– μΆ”μ²œ λ„μ„œ λͺ©λ‘")
403
-
404
- with gr.Row():
405
- book_display = gr.Markdown("뢄석 ν›„ μΆ”μ²œ λ„μ„œκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.")
406
-
407
- with gr.Accordion("πŸ’¬ μ±… 선택 ν”Όλ“œλ°± (ν•™μŠ΅μ— λ°˜μ˜λ©λ‹ˆλ‹€)", open=True, elem_classes="feedback-section"):
408
- gr.Markdown("λ§ˆμŒμ— λ“œλŠ” 책이 μžˆλ‹€λ©΄ 제λͺ©μ„ μž…λ ₯ν•˜κ³  κΈ°λ‘ν•΄μ£Όμ„Έμš”!")
409
-
410
  with gr.Row():
411
- selected_book_input = gr.Textbox(
412
- label="μ„ νƒν•œ μ±… 제λͺ©",
413
- placeholder="예: 1984",
414
- scale=3
415
- )
416
- feedback_btn = gr.Button("βœ… 선택 기둝", scale=1)
417
-
418
- feedback_result = gr.Textbox(label="ν”Όλ“œλ°± κ²°κ³Ό", interactive=False)
419
-
420
  with gr.Accordion("πŸ“ˆ ν”Όλ“œλ°± 톡계", open=False):
421
- stats_display = gr.Markdown("톡계λ₯Ό ν™•μΈν•˜λ €λ©΄ μƒˆλ‘œκ³ μΉ¨ λ²„νŠΌμ„ λˆ„λ₯΄μ„Έμš”.")
422
-
423
- def show_stats():
424
- feedback_data = load_feedback_data()
425
- total = len(feedback_data)
426
-
427
- if total == 0:
428
- return "πŸ“Š 아직 ν”Όλ“œλ°± 데이터가 μ—†μŠ΅λ‹ˆλ‹€."
429
-
430
- emotion_counts = {}
431
- for entry in feedback_data:
432
- emo = entry.get("detected_emotion", "Unknown")
433
- emotion_counts[emo] = emotion_counts.get(emo, 0) + 1
434
-
435
- stats_text = f"**총 ν”Όλ“œλ°± 수:** {total}\n\n"
436
- stats_text += "**감정별 뢄석 μš”μ²­:**\n\n"
437
- for emo, count in sorted(emotion_counts.items(), key=lambda x: x[1], reverse=True):
438
- stats_text += f"- {emo}: {count}회\n"
439
-
440
- return stats_text
441
-
442
  refresh_stats_btn = gr.Button("πŸ”„ 톡계 μƒˆλ‘œκ³ μΉ¨")
443
- refresh_stats_btn.click(fn=show_stats, outputs=stats_display)
444
-
445
- def update_ui(result):
446
- if result["status"] == "success":
447
- books_md = f"**{result['message']}**\n\n"
448
-
449
- if result["books"]:
450
- for i, book in enumerate(result["books"], 1):
451
- books_md += f"""
452
- ### {i}. {book['title']}
453
- - **μœ μ‚¬λ„ 점수:** {book['similarity']}
454
- - **μ΅œμ’… 점수:** {book['final_score']} (ν”Όλ“œλ°± 반영)
455
- - **쀄거리:** {book['content']}
456
-
457
- ---
458
- """
459
- else:
460
- books_md += "μΆ”μ²œν•  책을 μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."
461
-
462
- return (
463
- result,
464
- books_md,
465
- result["user_input"],
466
- result["emotion_label"],
467
- result["books"]
468
- )
469
- else:
470
- return (
471
- result,
472
- f"⚠️ {result.get('message', '였λ₯˜ λ°œμƒ')}",
473
- "",
474
- "",
475
- []
476
- )
477
-
478
  analyze_btn.click(
479
- fn=process_voice,
480
  inputs=audio_in,
481
- outputs=output_json
482
- ).then(
483
- fn=update_ui,
484
- inputs=output_json,
485
- outputs=[output_json, book_display, state_text, state_emotion, state_books]
 
 
486
  )
487
-
488
- feedback_btn.click(
489
- fn=record_book_selection,
490
- inputs=[state_text, state_emotion, state_books, selected_book_input],
491
- outputs=feedback_result
492
  )
493
 
494
  if __name__ == "__main__":
495
- demo.launch()
 
1
+ """
2
+ Boolook - μŒμ„± 기반 감정 뢄석 μ±… μΆ”μ²œ (HuggingFace Spaces)
3
+ record_cat.py μ•Œκ³ λ¦¬μ¦˜ 기반 μ—…κ·Έλ ˆμ΄λ“œ 버전
4
+ DB 컬럼: isbn, title, author, publisher, content, img_url
5
+ """
6
+
7
  import gradio as gr
8
  import pandas as pd
9
  import numpy as np
10
  import torch
11
+ import pickle
 
12
  import json
13
+ import csv
14
+ import warnings
15
+ from pathlib import Path
16
  from datetime import datetime
17
+ from collections import defaultdict
18
+ from transformers import pipeline as hf_pipeline
19
+ from sentence_transformers import SentenceTransformer, util as sbert_util
20
+
21
+ warnings.filterwarnings("ignore")
22
 
23
+ # ============================================================
24
  # μ„€μ •
25
+ # ============================================================
26
+ BOOK_DB_PATH = Path("book_db_final.csv")
27
+ FEEDBACK_PATH = Path("user_feedback.csv")
28
+ SBERT_CACHE_PATH = Path("book_embeddings.pkl")
29
+ SAMPLE_RATE = 16000
30
+ MIN_FEEDBACK_FOR_TRAIN = 20
31
+
32
  device = 0 if torch.cuda.is_available() else -1
 
 
33
 
34
+ # ============================================================
35
+ # λͺ¨λΈ λ‘œλ”©
36
+ # ============================================================
37
  print("πŸš€ λͺ¨λΈ λ‘œλ”© 쀑...")
38
 
39
  try:
40
+ stt_model = hf_pipeline(
41
  "automatic-speech-recognition",
42
  model="openai/whisper-large-v3-turbo",
43
+ device=device,
44
  )
45
  print("βœ… STT λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
46
  except Exception as e:
47
+ print(f"⚠️ STT λ‘œλ“œ μ‹€νŒ¨: {e}")
48
  stt_model = None
49
 
50
  try:
51
+ sbert_model = SentenceTransformer("jhgan/ko-sroberta-multitask")
52
+ sbert_model.max_seq_length = 384
53
+ print("βœ… SBERT λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
 
 
 
 
54
  except Exception as e:
55
+ print(f"⚠️ SBERT λ‘œλ“œ μ‹€νŒ¨: {e}")
56
+ sbert_model = None
57
 
58
  try:
59
+ audio_emotion_pipeline = hf_pipeline(
60
+ "audio-classification",
61
+ model="superb/wav2vec2-base-superb-er",
62
+ device=device,
63
+ )
64
+ print("βœ… μŒμ„± 감정 λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
65
  except Exception as e:
66
+ print(f"⚠️ μŒμ„± 감정 λͺ¨λΈ λ‘œλ“œ μ‹€νŒ¨: {e}")
67
+ audio_emotion_pipeline = None
68
+
69
+ print("βœ… λͺ¨λΈ λ‘œλ”© μ™„λ£Œ!")
70
+
71
+ # ============================================================
72
+ # 감정 λ ˆμ΄λΈ” & μ„€λͺ…
73
+ # ============================================================
74
+ _EMOTION_DESCS = {
75
+ "기쁨": "ν–‰λ³΅ν•˜κ³  즐겁고 μœ μΎŒν•œ κΈ°λΆ„",
76
+ "μ‹ λ’°": "λ”°λœ»ν•˜κ³  μ•ˆμ •μ μ΄λ©° κ°€μ‘±κ³Ό μš°μ • 같은 μœ λŒ€κ°",
77
+ "곡포": "무섭고 κΈ΄μž₯되며 슀릴 μžˆλŠ” 곡포와 λΆˆμ•ˆ",
78
+ "λ†€λžŒ": "λ°˜μ „κ³Ό 좩격, μ˜ˆμƒμΉ˜ λͺ»ν•œ κ²½μ΄λ‘œμ›€",
79
+ "μŠ¬ν””": "μŠ¬ν”„κ³  μ™Έλ‘­κ³  이별과 μƒμ‹€μ˜ 감정",
80
+ "혐였": "뢀쑰리와 λΆˆν‰λ“±, μœ„μ„ μ— λŒ€ν•œ λΉ„νŒκ³Ό ν’μž",
81
+ "λΆ„λ…Έ": "뢄노와 μ €ν•­, 투쟁과 κ°ˆλ“±",
82
+ "κΈ°λŒ€": "μ„±μž₯κ³Ό 도전, λͺ¨ν—˜κ³Ό 희망",
83
+ }
84
+ _EMOTION_LABELS = list(_EMOTION_DESCS.keys())
85
+ _LABEL_EMBS = sbert_model.encode(list(_EMOTION_DESCS.values()), convert_to_tensor=True) if sbert_model else None
86
+
87
+ _AUDIO_LABEL_MAP = {"hap": "기쁨", "neu": "μ‹ λ’°", "sad": "μŠ¬ν””", "ang": "λΆ„λ…Έ"}
88
+
89
+ # ν•œκ΅­μ–΄ ν‚€μ›Œλ“œ 감정 보정
90
+ _KEYWORD_BOOSTS = {
91
+ "μŠ¬ν””": ["μŠ¬ν”„", "우울", "눈물", "νž˜λ“€", "μ™Έλ‘œ"],
92
+ "λΆ„λ…Έ": ["ν™”λ‚˜", "짜증", "μ—΄λ°›", "빑치", "μ–΅μšΈ"],
93
+ "기쁨": ["행볡", "μ’‹λ‹€", "기쁘", "즐겁", "μ‹ λ‚˜"],
94
+ "곡포": ["무섭", "두렡", "κ±±μ •", "λΆˆμ•ˆ"],
95
+ "λ†€λžŒ": ["λ†€λž", "깜짝", "좩격"],
96
+ "μ‹ λ’°": ["믿음", "μ‚¬λž‘", "λ”°λœ»", "고마"],
97
+ "κΈ°λŒ€": ["κΈ°λŒ€", "희망", "μ„€λ ˆ"],
98
+ }
99
+
100
+ # ============================================================
101
+ # μ„Έμ…˜ ν”Όλ“œλ°± (μ „μ—­)
102
+ # ============================================================
103
+ class SessionFeedback:
104
+ def __init__(self):
105
+ self.accepted_counts = defaultdict(int)
106
+ self.rejected_counts = defaultdict(int)
107
+
108
+ def score_multiplier(self, emotion: str) -> float:
109
+ acc = self.accepted_counts[emotion]
110
+ rej = self.rejected_counts[emotion]
111
+ return max(0.5, 1.0 + (0.1 * acc) - (0.1 * rej))
112
 
113
+ _session = SessionFeedback()
114
 
115
+ # ============================================================
116
+ # λ„μ„œ 데이터 & μž„λ² λ”© λ‘œλ“œ
117
+ # ============================================================
118
  def load_book_data():
119
+ if not BOOK_DB_PATH.exists():
120
+ raise FileNotFoundError(f"❌ {BOOK_DB_PATH} 파일이 ν•„μš”ν•©λ‹ˆλ‹€.")
121
+
122
+ df = pd.read_csv(BOOK_DB_PATH, encoding="utf-8-sig").fillna("")
123
+ print(f"πŸ“š {len(df)}ꢌ λ‘œλ“œ μ™„λ£Œ")
124
+
125
+ # μž„λ² λ”© μΊμ‹œ
126
+ if SBERT_CACHE_PATH.exists():
127
+ print("βœ… μž„λ² λ”© μΊμ‹œ λ‘œλ“œ")
128
+ with open(SBERT_CACHE_PATH, "rb") as f:
129
+ emb_cache = pickle.load(f)
130
+ else:
131
+ emb_cache = {}
132
+
133
+ # μΊμ‹œμ— μ—†λŠ” μ±…λ§Œ μƒˆλ‘œ 계산
134
+ missing = [i for i, row in df.iterrows() if str(row["isbn"]) not in emb_cache]
135
+ if missing and sbert_model:
136
+ print(f"βš™οΈ μ‹ κ·œ μž„λ² λ”© 계산: {len(missing)}ꢌ")
137
+ texts = [
138
+ (str(df.at[i, "title"]) + " " + str(df.at[i, "content"]))[:500]
139
+ for i in missing
140
+ ]
141
+ vecs = sbert_model.encode(texts, convert_to_tensor=False, show_progress_bar=True)
142
+ for i, vec in zip(missing, vecs):
143
+ emb_cache[str(df.at[i, "isbn"])] = vec
144
+ with open(SBERT_CACHE_PATH, "wb") as f:
145
+ pickle.dump(emb_cache, f)
146
+ print("βœ… μž„λ² λ”© μ €μž₯ μ™„λ£Œ")
147
+
148
+ # DataFrame μˆœμ„œμ— 맞게 μž„λ² λ”© ν–‰λ ¬ ꡬ성
149
+ emb_matrix = np.stack([
150
+ emb_cache.get(str(row["isbn"]), np.zeros(384))
151
+ for _, row in df.iterrows()
152
+ ])
153
+ book_embeddings = torch.tensor(emb_matrix, dtype=torch.float32)
154
+
155
  return df, book_embeddings
156
 
157
  df, book_embeddings = load_book_data()
158
 
159
+ # ============================================================
160
+ # CatBoost κ°œμΈν™” λͺ¨λΈ (선택적)
161
+ # ============================================================
162
+ _ml_model = None
163
+ _ml_feature_names = []
 
 
 
 
 
 
 
 
164
 
165
+ def _try_load_catboost():
166
+ global _ml_model, _ml_feature_names
167
+ model_path = Path("catboost_recommender.cbm")
168
+ encoder_path = Path("feature_encoder.pkl")
 
 
 
 
 
 
 
 
 
 
 
 
169
  try:
170
+ from catboost import CatBoostClassifier
171
+ if model_path.exists():
172
+ _ml_model = CatBoostClassifier()
173
+ _ml_model.load_model(str(model_path))
174
+ if encoder_path.exists():
175
+ with open(encoder_path, "rb") as f:
176
+ enc = pickle.load(f)
177
+ print("βœ… CatBoost λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
178
+ return True
179
  except Exception as e:
180
+ print(f"⚠️ CatBoost λ‘œλ“œ μ‹€νŒ¨: {e}")
181
+ return False
182
 
183
+ _try_load_catboost()
184
+
185
+ def _ml_predict(isbn: str, emotion: str, content_len: int) -> float:
186
+ if _ml_model is None:
187
+ return 0.5
 
188
  try:
189
+ X = pd.DataFrame([{
190
+ "emotion": emotion,
191
+ "rank": 1,
192
+ "input_mode": "1",
193
+ "content_length": min(content_len, 500),
194
+ "has_content": 1 if content_len > 50 else 0,
195
+ }])
196
+ return float(_ml_model.predict_proba(X)[0][1])
197
+ except Exception:
198
+ return 0.5
199
+
200
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # 감정 뢄석
202
+ # ============================================================
203
+ def text_emotion_scores(text: str) -> dict:
204
+ """SBERT μ œλ‘œμƒ· + ν‚€μ›Œλ“œ 보정 ν˜Όν•©"""
205
+ scores = {emo: 0.0 for emo in _EMOTION_LABELS}
206
+
207
+ if sbert_model and _LABEL_EMBS is not None:
208
+ user_emb = sbert_model.encode(text, convert_to_tensor=True)
209
+ cos_scores = sbert_util.cos_sim(user_emb, _LABEL_EMBS)[0]
210
+ for i, label in enumerate(_EMOTION_LABELS):
211
+ scores[label] = cos_scores[i].item()
212
+
213
+ # ν‚€μ›Œλ“œ 보정
214
+ text_lower = text.lower()
215
+ for emotion, keywords in _KEYWORD_BOOSTS.items():
216
+ for kw in keywords:
217
+ if kw in text_lower:
218
+ scores[emotion] += 0.15
219
+ break
220
+
221
+ # μ •κ·œν™”
222
+ total = sum(scores.values())
223
+ if total > 0:
224
+ scores = {k: v / total for k, v in scores.items()}
225
+
226
+ return scores
227
+
228
+ def audio_emotion_scores(audio_array: np.ndarray, sr: int) -> dict:
229
+ """μŒμ„± μ‹ ν˜Έ 감정 λΆ„λ₯˜"""
230
+ scores = {emo: 0.0 for emo in _EMOTION_LABELS}
231
+ if audio_emotion_pipeline is None:
232
+ return scores
233
  try:
234
+ import scipy.io.wavfile as wav_io
235
+ tmp = "/tmp/_gradio_voice.wav"
236
+ wav_io.write(tmp, sr, (audio_array * 32767).astype(np.int16))
237
+ results = audio_emotion_pipeline(tmp)
238
+ for item in results:
239
+ mapped = _AUDIO_LABEL_MAP.get(item["label"])
 
 
240
  if mapped:
241
+ scores[mapped] += item["score"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  except Exception as e:
243
+ print(f"⚠️ μŒμ„± 감정 뢄석 였λ₯˜: {e}")
244
+ return scores
245
+
246
+ def fused_emotion(t_scores: dict, a_scores: dict) -> tuple:
247
+ a_max = max(a_scores.values()) or 1.0
248
+ a_norm = {e: v / a_max for e, v in a_scores.items()}
249
+ combined = {
250
+ emo: (t_scores[emo] * 0.6) + (a_norm[emo] * 0.4)
251
+ for emo in _EMOTION_LABELS
252
+ }
253
+ return max(combined, key=combined.get), combined
254
+
255
+ # ============================================================
256
+ # μΆ”μ²œ
257
+ # ============================================================
258
+ def get_recommendations(user_text: str, emotion: str, top_n: int = 3) -> list:
259
+ if sbert_model is None or df.empty:
260
  return []
261
+
262
+ session_w = _session.score_multiplier(emotion)
263
+ user_vec = sbert_model.encode(user_text, convert_to_tensor=True)
264
+ cos_sims = sbert_util.cos_sim(user_vec, book_embeddings)[0].cpu().numpy()
265
+
266
+ # ν”Όλ“œλ°± κ°€μ€‘μΉ˜
267
+ fb_weights = _load_feedback_weights()
268
+
269
+ results = []
270
+ for idx, (_, row) in enumerate(df.iterrows()):
271
+ content_len = len(str(row.get("content", "")))
272
+ ml_score = _ml_predict(str(row["isbn"]), emotion, content_len)
273
+ fb_boost = fb_weights.get((emotion, str(row["title"])), 0) * 0.1
274
+ cosine = float(cos_sims[idx])
275
+ final = cosine * 0.6 * session_w + ml_score * 0.4 + fb_boost
276
+
277
+ results.append({
278
+ "isbn": str(row.get("isbn", "")),
279
+ "title": str(row.get("title", "")),
280
+ "author": str(row.get("author", "-")),
281
+ "publisher": str(row.get("publisher", "-")),
282
+ "content": str(row.get("content", ""))[:150],
283
+ "img_url": str(row.get("img_url", "")),
284
+ "cosine": round(cosine, 3),
285
+ "ml_score": round(ml_score, 3),
286
+ "final": round(final, 3),
287
+ })
288
+
289
+ results.sort(key=lambda x: x["final"], reverse=True)
290
+ return results[:top_n]
291
+
292
+ # ============================================================
293
+ # ν”Όλ“œλ°± μ €μž₯ & λ‘œλ“œ
294
+ # ============================================================
295
+ def _load_feedback_weights() -> dict:
296
+ if not FEEDBACK_PATH.exists():
297
+ return {}
298
+ try:
299
+ fb_df = pd.read_csv(FEEDBACK_PATH, encoding="utf-8-sig", on_bad_lines="skip")
300
+ weights = {}
301
+ for _, row in fb_df.iterrows():
302
+ key = (str(row.get("emotion", "")), str(row.get("title", "")))
303
+ accepted = int(row.get("accepted", 0))
304
+ weights[key] = weights.get(key, 0) + (1.0 if accepted == 1 else -0.5)
305
+ return weights
306
+ except Exception:
307
+ return {}
308
+
309
+ def save_feedback_csv(isbn: str, title: str, emotion: str, accepted: int, rank: int):
310
+ pd.DataFrame([{
311
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
312
+ "isbn": isbn,
313
+ "title": title.replace("\n", " ").replace("\r", " "),
314
+ "emotion": emotion,
315
+ "accepted": accepted,
316
+ "input_mode": "gradio",
317
+ "rank": rank,
318
+ }]).to_csv(
319
+ FEEDBACK_PATH, mode="a", index=False,
320
+ header=not FEEDBACK_PATH.exists(),
321
+ encoding="utf-8-sig",
322
+ quoting=csv.QUOTE_ALL,
323
+ )
324
+ if accepted == 1:
325
+ _session.accepted_counts[emotion] += 1
326
+ else:
327
+ _session.rejected_counts[emotion] += 1
328
+
329
+ def get_feedback_stats() -> str:
330
+ if not FEEDBACK_PATH.exists():
331
+ return "πŸ“Š 아직 ν”Όλ“œλ°± 데이터가 μ—†μŠ΅λ‹ˆλ‹€."
332
  try:
333
+ fb_df = pd.read_csv(FEEDBACK_PATH, encoding="utf-8-sig", on_bad_lines="skip")
334
+ total = len(fb_df)
335
+ if total == 0:
336
+ return "πŸ“Š 아직 ν”Όλ“œλ°± 데이터가 μ—†μŠ΅λ‹ˆλ‹€."
337
+
338
+ emo_counts = fb_df.groupby("emotion")["accepted"].agg(["count", "sum"])
339
+ lines = [f"**총 ν”Όλ“œλ°±: {total}건**\n"]
340
+ for emo, row_s in emo_counts.iterrows():
341
+ rate = int(row_s["sum"]) / int(row_s["count"]) * 100
342
+ lines.append(f"- {emo}: {int(row_s['count'])}건 (수락λ₯  {rate:.0f}%)")
343
+ return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  except Exception as e:
345
+ return f"⚠️ 톡계 λ‘œλ“œ μ‹€νŒ¨: {e}"
 
346
 
347
+ # ============================================================
348
  # 메인 처리 ν•¨μˆ˜
349
+ # ============================================================
350
  def process_voice(audio_input):
351
+ """Gradio μŒμ„± μž…λ ₯ β†’ STT β†’ 감정 뢄석 β†’ μΆ”μ²œ"""
 
 
 
 
 
 
 
 
 
 
352
  if audio_input is None:
353
+ return "🎀 μŒμ„±μ„ λ…ΉμŒν•΄μ£Όμ„Έμš”.", "", "", [], ""
354
+
 
355
  if stt_model is None:
356
+ return "❌ STT λͺ¨λΈμ΄ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.", "", "", [], ""
357
+
 
358
  try:
359
  sr, y = audio_input
360
+ y = y.astype(np.float32)
361
+ max_v = np.max(np.abs(y))
362
+ if max_v > 0:
363
+ y = y / max_v
364
+
365
+ # STT
366
  stt_result = stt_model({"sampling_rate": sr, "raw": y})
367
+ user_text = stt_result["text"].strip()
368
+
369
+ if not user_text:
370
+ return "❌ μŒμ„±μ΄ μΈμ‹λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.", "", "", [], ""
371
+
372
+ # 감정 뢄석 (ν…μŠ€νŠΈ + μŒμ„± μœ΅ν•©)
373
+ t_scores = text_emotion_scores(user_text)
374
+ a_scores = audio_emotion_scores(y, sr)
375
+ top_label, combined = fused_emotion(t_scores, a_scores)
376
+ top3 = sorted(combined.items(), key=lambda x: x[1], reverse=True)[:3]
377
+ emotion_str = " | ".join(f"{e} {p:.2f}" for e, p in top3)
378
+
379
+ # μΆ”μ²œ
380
+ books = get_recommendations(user_text, top_label, top_n=3)
381
+
382
+ # μΆ”μ²œ κ²°κ³Ό λ§ˆν¬λ‹€μš΄
383
+ books_md = _render_books_md(books, top_label)
384
+
385
+ return user_text, top_label, emotion_str, books, books_md
386
+
387
+ except Exception as e:
388
+ return f"❌ 였λ₯˜: {e}", "", "", [], ""
389
+
390
+
391
+ def _render_books_md(books: list, emotion: str) -> str:
392
+ if not books:
393
+ return "μΆ”μ²œν•  책을 μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."
394
+
395
+ md = f"### πŸ“– [{emotion}] 감정에 μ–΄μšΈλ¦¬λŠ” μ±…\n\n"
396
+ for i, b in enumerate(books, 1):
397
+ ml_icon = "πŸ”₯" if b["ml_score"] > 0.7 else ("✨" if b["ml_score"] > 0.5 else "πŸ’‘")
398
+ md += f"**{ml_icon} {i}. {b['title']}**\n"
399
+ md += f"- μ €μž: {b['author']} | μΆœνŒμ‚¬: {b['publisher']}\n"
400
+ md += f"- {b['content']}...\n"
401
+ md += f"- μœ μ‚¬λ„: `{b['cosine']}` | ML: `{b['ml_score']}` | μ΅œμ’…: `{b['final']}`\n"
402
+ if b["img_url"]:
403
+ md += f"- πŸ–ΌοΈ [ν‘œμ§€ 보기]({b['img_url']})\n"
404
+ md += "\n---\n"
405
+ return md
406
+
407
+
408
+ def on_feedback(books_state: list, emotion: str, rank_str: str, liked: bool):
409
+ """μ’‹μ•„μš”/μ‹«μ–΄μš” λ²„νŠΌ 클릭"""
410
+ try:
411
+ rank = int(rank_str) - 1
412
+ if not books_state or rank < 0 or rank >= len(books_state):
413
+ return "⚠️ 책을 λ¨Όμ € μΆ”μ²œλ°›μ•„μ£Όμ„Έμš”."
414
+ book = books_state[rank]
415
+ accepted = 1 if liked else 0
416
+ save_feedback_csv(book["isbn"], book["title"], emotion, accepted, rank + 1)
417
+ icon = "πŸ‘" if liked else "πŸ‘Ž"
418
+ return f"{icon} '{book['title']}' ν”Όλ“œλ°±μ΄ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ν•™μŠ΅μ— λ°˜μ˜λ©λ‹ˆλ‹€!"
419
  except Exception as e:
420
+ return f"⚠️ ν”Όλ“œλ°± μ €μž₯ μ‹€νŒ¨: {e}"
421
+
422
+ # ============================================================
 
 
 
 
 
 
 
 
 
423
  # Gradio UI
424
+ # ============================================================
425
  custom_css = """
426
+ .feedback-row { background: #f0f8ff; padding: 12px; border-radius: 8px; }
427
+ .book-card { border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin: 8px 0; }
 
 
 
428
  """
429
 
430
+ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="Boolook πŸ“š") as demo:
431
+
432
+ gr.Markdown("""
433
+ # πŸ“š Boolook β€” μŒμ„± 기반 감정 뢄석 μ±… μΆ”μ²œ
434
+ λ‹Ήμ‹ μ˜ 감정을 말둜 ν‘œν˜„ν•˜λ©΄, AIκ°€ λΆ„μ„ν•˜μ—¬ λ”± λ§žλŠ” 책을 μΆ”μ²œν•΄λ“œλ¦½λ‹ˆλ‹€.
435
+
436
+ 🎀 **μ‚¬μš© 방법:**
437
+ 1. 마이크 λ²„νŠΌμ„ 눌러 ν˜„μž¬ 감정을 ν‘œν˜„ν•˜μ„Έμš”
438
+ 2. "λΆ„μ„ν•˜κΈ°" λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”
439
+ 3. μΆ”μ²œλœ 책에 πŸ‘ / πŸ‘Ž ν”Όλ“œλ°±μ„ μ£Όμ„Έμš” (ν•™μŠ΅μ— λ°˜μ˜λ©λ‹ˆλ‹€!)
440
+ """)
441
+
442
+ # μƒνƒœ μ €μž₯
443
+ state_books = gr.State([])
 
 
444
  state_emotion = gr.State("")
445
+
 
446
  with gr.Row():
447
  with gr.Column(scale=1):
448
  gr.Markdown("### 🎀 μŒμ„± μž…λ ₯")
449
  audio_in = gr.Audio(
450
  label="마이크둜 감정 ν‘œν˜„ν•˜κΈ°",
451
  sources=["microphone"],
452
+ type="numpy",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  )
454
+ analyze_btn = gr.Button("πŸ” λΆ„μ„ν•˜κΈ°", variant="primary", size="lg")
455
+ gr.Markdown("""
456
+ **πŸ’‘ μ˜ˆμ‹œ:**
457
+ - "였늘 λ„ˆλ¬΄ μŠ¬νΌμš”"
458
+ - "ν–‰λ³΅ν•œ κΈ°λΆ„μ΄μ—μš”"
459
+ - "ν™”κ°€ λ‚˜λŠ” 일이 μžˆμ—ˆμ–΄μš”"
460
+ """)
461
+
462
  with gr.Column(scale=1):
463
  gr.Markdown("### πŸ“Š 뢄석 κ²°κ³Ό")
464
+ out_text = gr.Textbox(label="μΈμ‹λœ ν…μŠ€νŠΈ", interactive=False)
465
+ out_emotion = gr.Textbox(label="κ°μ§€λœ 주감정", interactive=False)
466
+ out_emo_all = gr.Textbox(label="감정 뢄포 TOP3", interactive=False)
467
+
468
+ gr.Markdown("### πŸ“– μΆ”μ²œ λ„μ„œ")
469
+ out_books_md = gr.Markdown("뢄석 ν›„ μΆ”μ²œ λ„μ„œκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.")
470
+
471
+ with gr.Accordion("πŸ’¬ ν”Όλ“œλ°± (ν•™μŠ΅μ— 반영)", open=True, elem_classes="feedback-row"):
472
+ gr.Markdown("μΆ”μ²œλ°›μ€ μ±… 번호λ₯Ό μ„ νƒν•˜κ³  πŸ‘ / πŸ‘Ž 둜 ν‰κ°€ν•΄μ£Όμ„Έμš”!")
 
 
 
 
 
 
473
  with gr.Row():
474
+ rank_radio = gr.Radio(["1", "2", "3"], label="μ±… 번호", value="1")
475
+ like_btn = gr.Button("πŸ‘ μ’‹μ•„μš”", variant="primary")
476
+ dislike_btn = gr.Button("πŸ‘Ž μ‹«μ–΄μš”", variant="secondary")
477
+ feedback_out = gr.Textbox(label="ν”Όλ“œοΏ½οΏ½οΏ½ κ²°κ³Ό", interactive=False)
478
+
 
 
 
 
479
  with gr.Accordion("πŸ“ˆ ν”Όλ“œλ°± 톡계", open=False):
480
+ stats_md = gr.Markdown("μƒˆλ‘œκ³ μΉ¨μ„ 눌러 ν™•μΈν•˜μ„Έμš”.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  refresh_stats_btn = gr.Button("πŸ”„ 톡계 μƒˆλ‘œκ³ μΉ¨")
482
+ refresh_stats_btn.click(fn=get_feedback_stats, outputs=stats_md)
483
+
484
+ # ── 이벀트 μ—°κ²° ──
485
+ def run_analysis(audio):
486
+ user_text, emotion, emo_all, books, books_md = process_voice(audio)
487
+ return user_text, emotion, emo_all, books, emotion, books_md
488
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  analyze_btn.click(
490
+ fn=run_analysis,
491
  inputs=audio_in,
492
+ outputs=[out_text, out_emotion, out_emo_all, state_books, state_emotion, out_books_md],
493
+ )
494
+
495
+ like_btn.click(
496
+ fn=lambda books, emo, rank: on_feedback(books, emo, rank, liked=True),
497
+ inputs=[state_books, state_emotion, rank_radio],
498
+ outputs=feedback_out,
499
  )
500
+ dislike_btn.click(
501
+ fn=lambda books, emo, rank: on_feedback(books, emo, rank, liked=False),
502
+ inputs=[state_books, state_emotion, rank_radio],
503
+ outputs=feedback_out,
 
504
  )
505
 
506
  if __name__ == "__main__":
507
+ demo.launch()