minjune121 commited on
Commit
f428a77
Β·
verified Β·
1 Parent(s): 5d65be7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -77
app.py CHANGED
@@ -4,6 +4,8 @@ Boolook - μŒμ„± 기반 감정 뢄석 μ±… μΆ”μ²œ (HuggingFace Spaces)
4
  - μž„λ² λ”© λ‘œλ”©μ„ λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œλ‘œ 뢄리 (νƒ€μž„μ•„μ›ƒ λ°©μ§€)
5
  - 배치 크기 128둜 증가 (속도 ν–₯상)
6
  - μ„œλ²„κ°€ λ¨Όμ € μ—΄λ¦° λ’€ 데이터 λ‘œλ”© μ§„ν–‰
 
 
7
  """
8
 
9
  import gradio as gr
@@ -12,6 +14,7 @@ import numpy as np
12
  import torch
13
  import pickle
14
  import csv
 
15
  import threading
16
  import warnings
17
  import logging
@@ -29,11 +32,11 @@ logger = logging.getLogger(__name__)
29
  # ============================================================
30
  # μ„€μ •
31
  # ============================================================
32
- BOOK_DB_PATH = Path("book_db_final.csv")
33
- FEEDBACK_PATH = Path("user_feedback.csv")
34
  SBERT_CACHE_PATH = Path("book_embeddings.pkl")
35
- SAMPLE_RATE = 16000
36
- MAX_EMBEDDING_BATCH = 128 # 32 β†’ 128 (속도 ν–₯상)
37
 
38
  device = 0 if torch.cuda.is_available() else -1
39
  logger.info(f"πŸ–₯️ λ””λ°”μ΄μŠ€: {'GPU' if device == 0 else 'CPU'}")
@@ -47,7 +50,7 @@ _data_ready = False
47
  _data_lock = threading.Lock()
48
 
49
  # ============================================================
50
- # λͺ¨λΈ λ‘œλ”© (μŠ€νƒ€νŠΈμ—… μ‹œ 동기 μˆ˜ν–‰ - 빠름)
51
  # ============================================================
52
  logger.info("πŸš€ λͺ¨λΈ λ‘œλ”© 쀑...")
53
 
@@ -156,7 +159,6 @@ def load_book_data():
156
  logger.error(f"❌ CSV λ‘œλ“œ μ‹€νŒ¨: {e}")
157
  return
158
 
159
- # μž„λ² λ”© μΊμ‹œ λ‘œλ“œ
160
  emb_cache = {}
161
  if SBERT_CACHE_PATH.exists():
162
  try:
@@ -166,7 +168,6 @@ def load_book_data():
166
  except Exception as e:
167
  logger.warning(f"⚠️ μΊμ‹œ λ‘œλ“œ μ‹€νŒ¨: {e}")
168
 
169
- # μ‹ κ·œ μž„λ² λ”© 계산
170
  missing = [i for i, row in _df.iterrows() if str(row["isbn"]) not in emb_cache]
171
  if missing and sbert_model:
172
  logger.info(f"βš™οΈ μ‹ κ·œ μž„λ² λ”© 계산: {len(missing)}ꢌ")
@@ -192,7 +193,6 @@ def load_book_data():
192
  except Exception as e:
193
  logger.error(f"⚠️ μž„λ² λ”© 계산 μ‹€νŒ¨: {e}")
194
 
195
- # μž„λ² λ”© ν–‰λ ¬ ꡬ성
196
  try:
197
  emb_matrix = np.stack([
198
  emb_cache.get(str(row["isbn"]), np.zeros(384))
@@ -212,7 +212,6 @@ def load_book_data():
212
 
213
  logger.info("πŸŽ‰ λ°±κ·ΈλΌμš΄λ“œ 데이터 λ‘œλ“œ μ™„λ£Œ!")
214
 
215
- # λ°±κ·ΈλΌμš΄λ“œλ‘œ μ‹€ν–‰ (μ„œλ²„ 열리기 전에 μŠ€λ ˆλ“œλ§Œ μ‹œμž‘)
216
  threading.Thread(target=load_book_data, daemon=True).start()
217
 
218
  # ============================================================
@@ -224,7 +223,7 @@ def text_emotion_scores(text: str) -> Dict[str, float]:
224
  return scores
225
 
226
  try:
227
- user_emb = sbert_model.encode(text, convert_to_tensor=True, show_progress_bar=False)
228
  cos_scores = sbert_util.cos_sim(user_emb, _LABEL_EMBS)[0]
229
  for i, label in enumerate(_EMOTION_LABELS):
230
  scores[label] = float(cos_scores[i].item())
@@ -243,6 +242,7 @@ def text_emotion_scores(text: str) -> Dict[str, float]:
243
  scores = {k: v / total for k, v in scores.items()}
244
  return scores
245
 
 
246
  def audio_emotion_scores(audio_array: np.ndarray, sr: int) -> Dict[str, float]:
247
  scores = {emo: 0.0 for emo in _EMOTION_LABELS}
248
  if audio_emotion_pipeline is None:
@@ -263,6 +263,7 @@ def audio_emotion_scores(audio_array: np.ndarray, sr: int) -> Dict[str, float]:
263
  logger.warning(f"⚠️ μŒμ„± 감정 μ‹€νŒ¨: {e}")
264
  return scores
265
 
 
266
  def fused_emotion(t_scores: Dict[str, float], a_scores: Dict[str, float]) -> Tuple[str, Dict[str, float]]:
267
  if all(v == 0 for v in a_scores.values()):
268
  combined = t_scores
@@ -320,6 +321,31 @@ def get_recommendations(user_text: str, emotion: str, top_n: int = 3) -> List[Di
320
  logger.error(f"⚠️ μΆ”μ²œ μ‹€νŒ¨: {e}")
321
  return []
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  # ============================================================
324
  # ν”Όλ“œλ°±
325
  # ============================================================
@@ -337,6 +363,7 @@ def _load_feedback_weights() -> Dict[Tuple[str, str], float]:
337
  except Exception:
338
  return {}
339
 
 
340
  def save_feedback_csv(isbn: str, title: str, emotion: str, accepted: int, rank: int):
341
  try:
342
  data = {
@@ -359,6 +386,7 @@ def save_feedback_csv(isbn: str, title: str, emotion: str, accepted: int, rank:
359
  except Exception as e:
360
  logger.error(f"⚠️ ν”Όλ“œλ°± μ €μž₯ μ‹€νŒ¨: {e}")
361
 
 
362
  def get_feedback_stats() -> str:
363
  if not FEEDBACK_PATH.exists():
364
  return "πŸ“Š 아직 ν”Όλ“œλ°±μ΄ μ—†μŠ΅λ‹ˆλ‹€."
@@ -382,23 +410,17 @@ def get_feedback_stats() -> str:
382
  # 메인 처리
383
  # ============================================================
384
  def process_voice(audio_input):
385
- # ── 데이터 λ‘œλ”© 쀑 μ•ˆλ‚΄ ──
386
  if not _data_ready:
387
- return (
388
- "⏳ λ„μ„œ 데이터λ₯Ό λ‘œλ”© μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.",
389
- "", "", [], "",
390
- )
391
-
392
  if audio_input is None:
393
- return "🎀 μŒμ„±μ„ λ…ΉμŒν•΄μ£Όμ„Έμš”.", "", "", [], ""
394
-
395
  if stt_model is None:
396
- return "❌ STT λͺ¨λΈμ΄ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.", "", "", [], ""
397
 
398
  try:
399
  sr, y = audio_input
400
  if len(y) == 0:
401
- return "❌ μŒμ„±μ΄ λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€.", "", "", [], ""
402
 
403
  y = y.astype(np.float32)
404
  max_v = np.max(np.abs(y))
@@ -409,38 +431,21 @@ def process_voice(audio_input):
409
  user_text = stt_result["text"].strip()
410
 
411
  if not user_text:
412
- return "❌ μŒμ„±μ΄ μΈμ‹λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.", "", "", [], ""
413
 
414
  t_scores = text_emotion_scores(user_text)
415
  a_scores = audio_emotion_scores(y, sr)
416
  top_label, combined = fused_emotion(t_scores, a_scores)
417
 
418
- top3 = sorted(combined.items(), key=lambda x: x[1], reverse=True)[:3]
419
- emotion_str = " | ".join(f"{e} {p:.2f}" for e, p in top3)
420
-
421
- books = get_recommendations(user_text, top_label, top_n=3)
422
- books_md = _render_books_md(books, top_label)
423
 
424
- return user_text, top_label, emotion_str, books, books_md
425
 
426
  except Exception as e:
427
  logger.error(f"❌ 처리 였λ₯˜: {e}")
428
- return f"❌ 였λ₯˜: {str(e)}", "", "", [], ""
429
 
430
- def _render_books_md(books: List[Dict], emotion: str) -> str:
431
- if not books:
432
- return "πŸ˜” μΆ”μ²œν•  책을 μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."
433
-
434
- md = f"### πŸ“– [{emotion}] 감정에 μ–΄μšΈλ¦¬λŠ” μ±…\n\n"
435
- for i, b in enumerate(books, 1):
436
- md += f"**{i}. {b['title']}**\n"
437
- md += f"- μ €μž: {b['author']} | μΆœνŒμ‚¬: {b['publisher']}\n"
438
- md += f"- {b['content']}...\n"
439
- md += f"- μΆ”μ²œ 점수: `{b['score']}`\n"
440
- if b["img_url"]:
441
- md += f"- πŸ–ΌοΈ [ν‘œμ§€ 보기]({b['img_url']})\n"
442
- md += "\n---\n"
443
- return md
444
 
445
  def on_feedback(books_state: list, emotion: str, rank_str: str, liked: bool):
446
  try:
@@ -455,27 +460,15 @@ def on_feedback(books_state: list, emotion: str, rank_str: str, liked: bool):
455
  except Exception as e:
456
  return f"⚠️ ν”Όλ“œλ°± μ €μž₯ μ‹€νŒ¨: {e}"
457
 
458
- # ============================================================
459
- # Gradio UI
460
- # ============================================================
461
- css = """
462
- .feedback-row {
463
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
464
- padding: 20px;
465
- border-radius: 15px;
466
- margin: 15px 0;
467
- }
468
- .feedback-row * { color: white !important; }
469
- """
470
 
471
  def run_analysis(audio):
472
- result = process_voice(audio)
473
- # result: (text, emotion, emo_all, books, books_md)
474
- # outputs: [out_text, out_emotion, out_emo_all, state_books, state_emotion, out_books_md]
475
- text, emo, emo_all, books, books_md = result
476
- return text, emo, emo_all, books, emo, books_md
477
 
478
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Boolook πŸ“š") as demo:
 
 
 
479
  gr.Markdown("""
480
  # πŸ“š Boolook β€” μŒμ„± 기반 감정 뢄석 μ±… μΆ”μ²œ
481
  λ‹Ήμ‹ μ˜ 감정을 말둜 ν‘œν˜„ν•˜λ©΄, AIκ°€ λ”± λ§žλŠ” 책을 μΆ”μ²œν•΄λ“œλ¦½λ‹ˆλ‹€.
@@ -489,28 +482,23 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Boolook πŸ“š") as demo:
489
  with gr.Row():
490
  with gr.Column(scale=1):
491
  gr.Markdown("### 🎀 μŒμ„± μž…λ ₯")
492
- audio_in = gr.Audio(
493
- label="마이크둜 감정 ν‘œν˜„ν•˜κΈ°",
494
- sources=["microphone"],
495
- type="numpy",
496
- )
497
  analyze_btn = gr.Button("πŸ” λΆ„μ„ν•˜κΈ°", variant="primary", size="lg")
498
  gr.Markdown("πŸ’‘ 예: '였늘 λ„ˆλ¬΄ μŠ¬νΌμš”', 'ν–‰λ³΅ν•œ κΈ°λΆ„μ΄μ—μš”'")
499
 
500
  with gr.Column(scale=1):
501
- gr.Markdown("### πŸ“Š 뢄석 κ²°κ³Ό")
502
- out_text = gr.Textbox(label="μΈμ‹λœ ν…μŠ€νŠΈ", interactive=False)
503
- out_emotion = gr.Textbox(label="κ°μ§€λœ 주감정", interactive=False)
504
- out_emo_all = gr.Textbox(label="감정 뢄포 TOP3", interactive=False)
505
-
506
- out_books_md = gr.Markdown("### πŸ“– μΆ”μ²œ λ„μ„œ\n뢄석 ν›„ ν‘œμ‹œλ©λ‹ˆλ‹€.")
507
 
508
- with gr.Accordion("πŸ’¬ ν”Όλ“œλ°±", open=True, elem_classes="feedback-row"):
509
  gr.Markdown("μΆ”μ²œλ°›μ€ 책에 평가λ₯Ό λ‚¨κ²¨μ£Όμ„Έμš”!")
510
  with gr.Row():
511
- rank_radio = gr.Radio(["1", "2", "3"], label="μ±… 번호", value="1")
512
- like_btn = gr.Button("πŸ‘ μ’‹μ•„μš”", variant="primary")
513
- dislike_btn = gr.Button("πŸ‘Ž μ‹«μ–΄μš”", variant="secondary")
514
  feedback_out = gr.Textbox(label="ν”Όλ“œλ°± κ²°κ³Ό", interactive=False)
515
 
516
  with gr.Accordion("πŸ“ˆ 톡계", open=False):
@@ -518,11 +506,10 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Boolook πŸ“š") as demo:
518
  refresh_btn = gr.Button("πŸ”„ 톡계 μƒˆλ‘œκ³ μΉ¨")
519
  refresh_btn.click(fn=get_feedback_stats, outputs=stats_md)
520
 
521
- # 이벀트
522
  analyze_btn.click(
523
  fn=run_analysis,
524
  inputs=audio_in,
525
- outputs=[out_text, out_emotion, out_emo_all, state_books, state_emotion, out_books_md],
526
  )
527
  like_btn.click(
528
  fn=lambda b, e, r: on_feedback(b, e, r, True),
 
4
  - μž„λ² λ”© λ‘œλ”©μ„ λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œλ‘œ 뢄리 (νƒ€μž„μ•„μ›ƒ λ°©μ§€)
5
  - 배치 크기 128둜 증가 (속도 ν–₯상)
6
  - μ„œλ²„κ°€ λ¨Όμ € μ—΄λ¦° λ’€ 데이터 λ‘œλ”© μ§„ν–‰
7
+ - μΆ”μ²œ κ²°κ³Ό 좜λ ₯을 JSON ν˜•μ‹μœΌλ‘œ λ‹¨μˆœν™”
8
+ - emotion_score: 주감정 단일 수치
9
  """
10
 
11
  import gradio as gr
 
14
  import torch
15
  import pickle
16
  import csv
17
+ import json
18
  import threading
19
  import warnings
20
  import logging
 
32
  # ============================================================
33
  # μ„€μ •
34
  # ============================================================
35
+ BOOK_DB_PATH = Path("book_db_final.csv")
36
+ FEEDBACK_PATH = Path("user_feedback.csv")
37
  SBERT_CACHE_PATH = Path("book_embeddings.pkl")
38
+ SAMPLE_RATE = 16000
39
+ MAX_EMBEDDING_BATCH = 128
40
 
41
  device = 0 if torch.cuda.is_available() else -1
42
  logger.info(f"πŸ–₯️ λ””λ°”μ΄μŠ€: {'GPU' if device == 0 else 'CPU'}")
 
50
  _data_lock = threading.Lock()
51
 
52
  # ============================================================
53
+ # λͺ¨λΈ λ‘œλ”©
54
  # ============================================================
55
  logger.info("πŸš€ λͺ¨λΈ λ‘œλ”© 쀑...")
56
 
 
159
  logger.error(f"❌ CSV λ‘œλ“œ μ‹€νŒ¨: {e}")
160
  return
161
 
 
162
  emb_cache = {}
163
  if SBERT_CACHE_PATH.exists():
164
  try:
 
168
  except Exception as e:
169
  logger.warning(f"⚠️ μΊμ‹œ λ‘œλ“œ μ‹€νŒ¨: {e}")
170
 
 
171
  missing = [i for i, row in _df.iterrows() if str(row["isbn"]) not in emb_cache]
172
  if missing and sbert_model:
173
  logger.info(f"βš™οΈ μ‹ κ·œ μž„λ² λ”© 계산: {len(missing)}ꢌ")
 
193
  except Exception as e:
194
  logger.error(f"⚠️ μž„λ² λ”© 계산 μ‹€νŒ¨: {e}")
195
 
 
196
  try:
197
  emb_matrix = np.stack([
198
  emb_cache.get(str(row["isbn"]), np.zeros(384))
 
212
 
213
  logger.info("πŸŽ‰ λ°±κ·ΈλΌμš΄λ“œ 데이터 λ‘œλ“œ μ™„λ£Œ!")
214
 
 
215
  threading.Thread(target=load_book_data, daemon=True).start()
216
 
217
  # ============================================================
 
223
  return scores
224
 
225
  try:
226
+ user_emb = sbert_model.encode(text, convert_to_tensor=True, show_progress_bar=False)
227
  cos_scores = sbert_util.cos_sim(user_emb, _LABEL_EMBS)[0]
228
  for i, label in enumerate(_EMOTION_LABELS):
229
  scores[label] = float(cos_scores[i].item())
 
242
  scores = {k: v / total for k, v in scores.items()}
243
  return scores
244
 
245
+
246
  def audio_emotion_scores(audio_array: np.ndarray, sr: int) -> Dict[str, float]:
247
  scores = {emo: 0.0 for emo in _EMOTION_LABELS}
248
  if audio_emotion_pipeline is None:
 
263
  logger.warning(f"⚠️ μŒμ„± 감정 μ‹€νŒ¨: {e}")
264
  return scores
265
 
266
+
267
  def fused_emotion(t_scores: Dict[str, float], a_scores: Dict[str, float]) -> Tuple[str, Dict[str, float]]:
268
  if all(v == 0 for v in a_scores.values()):
269
  combined = t_scores
 
321
  logger.error(f"⚠️ μΆ”μ²œ μ‹€νŒ¨: {e}")
322
  return []
323
 
324
+ # ============================================================
325
+ # μΆ”μ²œ κ²°κ³Ό β†’ JSON λ Œλ”λ§
326
+ # ============================================================
327
+ def _render_books_json(user_text: str, emotion: str, combined: Dict[str, float], books: List[Dict]) -> str:
328
+ if not books:
329
+ return json.dumps({"error": "μΆ”μ²œν•  책을 μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."}, ensure_ascii=False, indent=2)
330
+
331
+ output = {
332
+ "user_text": user_text,
333
+ "emotion": emotion,
334
+ "emotion_score": round(combined.get(emotion, 0.0), 3),
335
+ "recommendations": [
336
+ {
337
+ "isbn": b["isbn"],
338
+ "title": b["title"],
339
+ "author": b["author"],
340
+ "publisher": b["publisher"],
341
+ "content": b["content"],
342
+ "img_url": b["img_url"],
343
+ }
344
+ for b in books
345
+ ],
346
+ }
347
+ return json.dumps(output, ensure_ascii=False, indent=2)
348
+
349
  # ============================================================
350
  # ν”Όλ“œλ°±
351
  # ============================================================
 
363
  except Exception:
364
  return {}
365
 
366
+
367
  def save_feedback_csv(isbn: str, title: str, emotion: str, accepted: int, rank: int):
368
  try:
369
  data = {
 
386
  except Exception as e:
387
  logger.error(f"⚠️ ν”Όλ“œλ°± μ €μž₯ μ‹€νŒ¨: {e}")
388
 
389
+
390
  def get_feedback_stats() -> str:
391
  if not FEEDBACK_PATH.exists():
392
  return "πŸ“Š 아직 ν”Όλ“œλ°±μ΄ μ—†μŠ΅λ‹ˆλ‹€."
 
410
  # 메인 처리
411
  # ============================================================
412
  def process_voice(audio_input):
 
413
  if not _data_ready:
414
+ return json.dumps({"error": "⏳ λ„μ„œ 데이터 λ‘œλ”© μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."}, ensure_ascii=False, indent=2), [], ""
 
 
 
 
415
  if audio_input is None:
416
+ return json.dumps({"error": "🎀 μŒμ„±μ„ λ…ΉμŒν•΄μ£Όμ„Έμš”."}, ensure_ascii=False, indent=2), [], ""
 
417
  if stt_model is None:
418
+ return json.dumps({"error": "❌ STT λͺ¨λΈμ΄ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}, ensure_ascii=False, indent=2), [], ""
419
 
420
  try:
421
  sr, y = audio_input
422
  if len(y) == 0:
423
+ return json.dumps({"error": "❌ μŒμ„±μ΄ λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€."}, ensure_ascii=False, indent=2), [], ""
424
 
425
  y = y.astype(np.float32)
426
  max_v = np.max(np.abs(y))
 
431
  user_text = stt_result["text"].strip()
432
 
433
  if not user_text:
434
+ return json.dumps({"error": "❌ μŒμ„±μ΄ μΈμ‹λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}, ensure_ascii=False, indent=2), [], ""
435
 
436
  t_scores = text_emotion_scores(user_text)
437
  a_scores = audio_emotion_scores(y, sr)
438
  top_label, combined = fused_emotion(t_scores, a_scores)
439
 
440
+ books = get_recommendations(user_text, top_label, top_n=3)
441
+ books_json = _render_books_json(user_text, top_label, combined, books)
 
 
 
442
 
443
+ return books_json, books, top_label
444
 
445
  except Exception as e:
446
  logger.error(f"❌ 처리 였λ₯˜: {e}")
447
+ return json.dumps({"error": str(e)}, ensure_ascii=False, indent=2), [], ""
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
  def on_feedback(books_state: list, emotion: str, rank_str: str, liked: bool):
451
  try:
 
460
  except Exception as e:
461
  return f"⚠️ ν”Όλ“œλ°± μ €μž₯ μ‹€νŒ¨: {e}"
462
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  def run_analysis(audio):
465
+ books_json, books, emotion = process_voice(audio)
466
+ return books_json, books, emotion
 
 
 
467
 
468
+ # ============================================================
469
+ # Gradio UI
470
+ # ============================================================
471
+ with gr.Blocks(theme=gr.themes.Soft(), title="Boolook πŸ“š") as demo:
472
  gr.Markdown("""
473
  # πŸ“š Boolook β€” μŒμ„± 기반 감정 뢄석 μ±… μΆ”μ²œ
474
  λ‹Ήμ‹ μ˜ 감정을 말둜 ν‘œν˜„ν•˜λ©΄, AIκ°€ λ”± λ§žλŠ” 책을 μΆ”μ²œν•΄λ“œλ¦½λ‹ˆλ‹€.
 
482
  with gr.Row():
483
  with gr.Column(scale=1):
484
  gr.Markdown("### 🎀 μŒμ„± μž…λ ₯")
485
+ audio_in = gr.Audio(sources=["microphone"], type="numpy", label="마이크둜 감정 ν‘œν˜„ν•˜κΈ°")
 
 
 
 
486
  analyze_btn = gr.Button("πŸ” λΆ„μ„ν•˜κΈ°", variant="primary", size="lg")
487
  gr.Markdown("πŸ’‘ 예: '였늘 λ„ˆλ¬΄ μŠ¬νΌμš”', 'ν–‰λ³΅ν•œ κΈ°λΆ„μ΄μ—μš”'")
488
 
489
  with gr.Column(scale=1):
490
+ out_books_json = gr.Code(
491
+ label="πŸ“Š 뢄석 κ²°κ³Ό & πŸ“– μΆ”μ²œ λ„μ„œ",
492
+ language="json",
493
+ interactive=False,
494
+ )
 
495
 
496
+ with gr.Accordion("πŸ’¬ ν”Όλ“œλ°±", open=True):
497
  gr.Markdown("μΆ”μ²œλ°›μ€ 책에 평가λ₯Ό λ‚¨κ²¨μ£Όμ„Έμš”!")
498
  with gr.Row():
499
+ rank_radio = gr.Radio(["1", "2", "3"], label="μ±… 번호", value="1")
500
+ like_btn = gr.Button("πŸ‘ μ’‹μ•„μš”", variant="primary")
501
+ dislike_btn = gr.Button("πŸ‘Ž μ‹«μ–΄μš”", variant="secondary")
502
  feedback_out = gr.Textbox(label="ν”Όλ“œλ°± κ²°κ³Ό", interactive=False)
503
 
504
  with gr.Accordion("πŸ“ˆ 톡계", open=False):
 
506
  refresh_btn = gr.Button("πŸ”„ 톡계 μƒˆλ‘œκ³ μΉ¨")
507
  refresh_btn.click(fn=get_feedback_stats, outputs=stats_md)
508
 
 
509
  analyze_btn.click(
510
  fn=run_analysis,
511
  inputs=audio_in,
512
+ outputs=[out_books_json, state_books, state_emotion],
513
  )
514
  like_btn.click(
515
  fn=lambda b, e, r: on_feedback(b, e, r, True),