sseo325 commited on
Commit
58cff79
Β·
verified Β·
1 Parent(s): 3db8345

Upload app_hf.py

Browse files
Files changed (1) hide show
  1. app_hf.py +595 -0
app_hf.py ADDED
@@ -0,0 +1,595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import zipfile
4
+ import uuid
5
+ from datetime import datetime
6
+
7
+ import gradio as gr
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ from PIL import Image
11
+ from fpdf import FPDF
12
+ import plotly.graph_objects as go
13
+
14
+
15
+ # =========================
16
+ # 1. μ„Έμ…˜ ZIP λ‘œλ”©
17
+ # =========================
18
+ def load_session(zip_path: str):
19
+ """
20
+ μ—…λ‘œλ“œλœ ZIP을 고유 μ„Έμ…˜ 폴더에 ν’€κ³ 
21
+ log.json, frames 디렉토리λ₯Ό μ€€λΉ„ν•œλ‹€.
22
+ """
23
+ base_dir = "uploaded_sessions"
24
+ os.makedirs(base_dir, exist_ok=True)
25
+
26
+ session_id = datetime.now().strftime("%Y%m%d_%H%M%S_") + str(uuid.uuid4())[:8]
27
+ session_dir = os.path.join(base_dir, session_id)
28
+ os.makedirs(session_dir, exist_ok=True)
29
+
30
+ with zipfile.ZipFile(zip_path, "r") as zf:
31
+ zf.extractall(session_dir)
32
+
33
+ log_path = os.path.join(session_dir, "log.json")
34
+ if not os.path.exists(log_path):
35
+ raise FileNotFoundError("log.json not found inside ZIP. Check collector.py output format.")
36
+
37
+ with open(log_path, "r", encoding="utf-8") as f:
38
+ data = json.load(f)
39
+
40
+ samples = data.get("samples", [])
41
+ events = data.get("events", [])
42
+ meta = data.get("meta", {})
43
+
44
+ return samples, events, meta, session_dir
45
+
46
+
47
+ # =========================
48
+ # 2. 보쑰 ν•¨μˆ˜λ“€
49
+ # =========================
50
+ def moving_average(arr, window=5):
51
+ arr = np.array(arr, dtype=float)
52
+ if len(arr) == 0:
53
+ return arr
54
+ if len(arr) < window:
55
+ return arr
56
+ cumsum = np.cumsum(np.insert(arr, 0, 0))
57
+ ma = (cumsum[window:] - cumsum[:-window]) / float(window)
58
+ # 길이 λ§žμΆ”κΈ°: μ•žλΆ€λΆ„μ€ κ·Έλƒ₯ 원본 μ‚¬μš©
59
+ head = arr[:window-1]
60
+ return np.concatenate([head, ma])
61
+
62
+
63
+ def normalize(arr):
64
+ arr = np.array(arr, dtype=float)
65
+ if arr.size == 0:
66
+ return arr
67
+ min_v = np.min(arr)
68
+ max_v = np.max(arr)
69
+ if max_v - min_v < 1e-8:
70
+ return np.zeros_like(arr)
71
+ return (arr - min_v) / (max_v - min_v)
72
+
73
+
74
+ # =========================
75
+ # 3. νƒ€μž„λΌμΈ 뢄석
76
+ # =========================
77
+ def analyze_timeline(samples, alpha: float, ma_window: int = 5):
78
+ """
79
+ samplesμ—μ„œ time, p_surprise, bpm을 μΆ”μΆœν•˜κ³ 
80
+ BPM smoothing + baseline + combined_score 계산.
81
+ """
82
+ if len(samples) == 0:
83
+ raise ValueError("No samples found in log.json.")
84
+
85
+ times = np.array([s.get("time", 0.0) for s in samples], dtype=float)
86
+ ps = np.array([s.get("p_surprise", 0.0) for s in samples], dtype=float)
87
+
88
+ raw_bpm = []
89
+ for s in samples:
90
+ b = s.get("bpm", None)
91
+ raw_bpm.append(0.0 if b is None else float(b))
92
+ raw_bpm = np.array(raw_bpm, dtype=float)
93
+
94
+ # BPM smoothing (moving average)
95
+ bpm_smooth = moving_average(raw_bpm, window=ma_window)
96
+
97
+ # baseline (유효 BPM만 median)
98
+ valid_bpm = bpm_smooth[bpm_smooth > 0]
99
+ if len(valid_bpm) > 0:
100
+ baseline = float(np.median(valid_bpm))
101
+ else:
102
+ baseline = 0.0
103
+
104
+ # spike component: baseline 이상 λΆ€λΆ„λ§Œ μ‚¬μš©
105
+ bpm_spike = np.where(bpm_smooth > baseline, bpm_smooth - baseline, 0.0)
106
+
107
+ # normalization
108
+ ps_norm = normalize(ps)
109
+ bpm_norm = normalize(bpm_spike)
110
+
111
+ beta = 1.0 - alpha
112
+ combined = alpha * ps_norm + beta * bpm_norm
113
+
114
+ return times, ps, bpm_smooth, combined, baseline
115
+
116
+
117
+ # =========================
118
+ # 4. Top3 + 간격 보정 + BPM κΈ‰μƒμŠΉ 탐지
119
+ # =========================
120
+ def pick_top3_events(events, times, combined, min_gap_sec=1.0):
121
+ """
122
+ 각 μ΄λ²€νŠΈμ— combined_scoreλ₯Ό λ§€κΈ°κ³ ,
123
+ μ΅œμ†Œ μ‹œκ°„ 간격 >= min_gap_sec 쑰건을 λ§Œμ‘±ν•˜λŠ” Top3 선택.
124
+ """
125
+ if len(events) == 0:
126
+ return []
127
+
128
+ times = np.array(times, dtype=float)
129
+ enriched = []
130
+
131
+ for e in events:
132
+ t = float(e.get("time", 0.0))
133
+ idx = int(np.argmin(np.abs(times - t)))
134
+ score = float(combined[idx]) if 0 <= idx < len(combined) else 0.0
135
+
136
+ enriched.append({
137
+ "time": t,
138
+ "p_surprise": float(e.get("p_surprise", 0.0)),
139
+ "bpm": float(e["bpm"]) if e.get("bpm") is not None else None,
140
+ "frame_file": e.get("frame_file", ""),
141
+ "combined_score": score
142
+ })
143
+
144
+ # combined_score κΈ°μ€€ μ •λ ¬
145
+ enriched.sort(key=lambda x: x["combined_score"], reverse=True)
146
+
147
+ selected = []
148
+ for cand in enriched:
149
+ if len(selected) == 0:
150
+ selected.append(cand)
151
+ else:
152
+ too_close = any(abs(cand["time"] - ev["time"]) < min_gap_sec for ev in selected)
153
+ if not too_close:
154
+ selected.append(cand)
155
+ if len(selected) >= 3:
156
+ break
157
+
158
+ return selected
159
+
160
+
161
+ def detect_bpm_spikes(times, bpm, baseline, spike_delta=10.0, min_gap_sec=1.0):
162
+ """
163
+ BPM이 짧은 μ‹œκ°„ μ•ˆμ— κΈ‰μƒμŠΉν•˜λŠ” ꡬ간 탐지.
164
+ κ°„λ‹¨νžˆ: bpm[i] - bpm[i-1] >= spike_delta 이고,
165
+ 이벀트 κ°„ μ‹œκ°„ 간격 >= min_gap_sec.
166
+ """
167
+ spikes = []
168
+ last_spike_t = -1e9
169
+ for i in range(1, len(bpm)):
170
+ if bpm[i-1] <= 0 or bpm[i] <= 0:
171
+ continue
172
+ diff = bpm[i] - bpm[i-1]
173
+ if diff >= spike_delta and bpm[i] > baseline:
174
+ t = times[i]
175
+ if t - last_spike_t >= min_gap_sec:
176
+ spikes.append(t)
177
+ last_spike_t = t
178
+ return spikes
179
+
180
+
181
+ # =========================
182
+ # 5. Plotly νƒ€μž„λΌμΈ + Heatmap
183
+ # =========================
184
+ def build_timeline_plot(times, ps, bpm, combined, top3, spike_times):
185
+ """
186
+ Plotly figure (p_surprise, BPM, combined + top3 marker + spike lines)
187
+ """
188
+ fig = go.Figure()
189
+
190
+ # p_surprise
191
+ fig.add_trace(
192
+ go.Scatter(
193
+ x=times,
194
+ y=ps,
195
+ mode="lines",
196
+ name="p_surprise",
197
+ line=dict(width=2)
198
+ )
199
+ )
200
+
201
+ # BPM (y2)
202
+ fig.add_trace(
203
+ go.Scatter(
204
+ x=times,
205
+ y=bpm,
206
+ mode="lines",
207
+ name="BPM (smoothed)",
208
+ line=dict(width=2, dash="dot"),
209
+ yaxis="y2"
210
+ )
211
+ )
212
+
213
+ # combined score
214
+ fig.add_trace(
215
+ go.Scatter(
216
+ x=times,
217
+ y=combined,
218
+ mode="lines",
219
+ name="combined_score",
220
+ line=dict(width=2)
221
+ )
222
+ )
223
+
224
+ # Top3 markers (combined score μœ„μ— ν‘œμ‹œ)
225
+ if top3:
226
+ top_times = [ev["time"] for ev in top3]
227
+ # 각 μ‹œκ°„μ—μ„œμ˜ combined κ°’
228
+ y_vals = []
229
+ for t in top_times:
230
+ idx = int(np.argmin(np.abs(times - t)))
231
+ if 0 <= idx < len(combined):
232
+ y_vals.append(combined[idx])
233
+ else:
234
+ y_vals.append(None)
235
+
236
+ fig.add_trace(
237
+ go.Scatter(
238
+ x=top_times,
239
+ y=y_vals,
240
+ mode="markers+text",
241
+ name="Top3",
242
+ marker=dict(size=12, symbol="star", color="gold"),
243
+ text=[f"Top{i+1}" for i in range(len(top3))],
244
+ textposition="top center"
245
+ )
246
+ )
247
+
248
+ # 심박 κΈ‰μƒμŠΉ ꡬ간: μ„Έλ‘œμ„ 
249
+ shapes = []
250
+ for t in spike_times:
251
+ shapes.append(
252
+ dict(
253
+ type="line",
254
+ x0=t,
255
+ x1=t,
256
+ y0=0,
257
+ y1=1,
258
+ xref="x",
259
+ yref="paper",
260
+ line=dict(color="red", width=1, dash="dot")
261
+ )
262
+ )
263
+
264
+ fig.update_layout(
265
+ title="Surprise Timeline (Expression + Heart-rate)",
266
+ xaxis=dict(title="Time (s)"),
267
+ yaxis=dict(title="p_surprise / combined_score", side="left"),
268
+ yaxis2=dict(
269
+ title="BPM (smoothed)",
270
+ overlaying="y",
271
+ side="right"
272
+ ),
273
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
274
+ shapes=shapes,
275
+ margin=dict(l=60, r=60, t=80, b=40)
276
+ )
277
+
278
+ return fig
279
+
280
+
281
+ def build_heatmap(times, combined, bins=50):
282
+ """
283
+ μ‹œκ°„μ— λ”°λ₯Έ combined_score heatmap 생성.
284
+ """
285
+ if len(times) == 0:
286
+ fig = go.Figure()
287
+ fig.update_layout(title="Surprise Intensity Heatmap (no data)")
288
+ return fig
289
+
290
+ t_min, t_max = float(np.min(times)), float(np.max(times))
291
+ if t_max <= t_min:
292
+ t_max = t_min + 1e-6
293
+
294
+ edges = np.linspace(t_min, t_max, bins + 1)
295
+ centers = (edges[:-1] + edges[1:]) / 2.0
296
+
297
+ z_row = []
298
+ for i in range(bins):
299
+ mask = (times >= edges[i]) & (times < edges[i+1])
300
+ if np.any(mask):
301
+ z_row.append(float(np.max(combined[mask])))
302
+ else:
303
+ z_row.append(0.0)
304
+
305
+ z = np.array([z_row])
306
+
307
+ fig = go.Figure(data=go.Heatmap(
308
+ x=centers,
309
+ y=["surprise"],
310
+ z=z,
311
+ colorscale="YlOrRd",
312
+ colorbar=dict(title="Intensity")
313
+ ))
314
+
315
+ fig.update_layout(
316
+ title="Surprise Intensity Heatmap (combined_score over time)",
317
+ xaxis=dict(title="Time (s)"),
318
+ yaxis=dict(showticklabels=False),
319
+ margin=dict(l=60, r=60, t=80, b=40)
320
+ )
321
+
322
+ return fig
323
+
324
+
325
+ # =========================
326
+ # 6. PDF 생성 (matplotlib 이용)
327
+ # =========================
328
+ def create_pdf(session_dir, meta, top3, times, ps, bpm, combined, alpha, beta, baseline):
329
+ reports_dir = os.path.join(session_dir, "reports")
330
+ os.makedirs(reports_dir, exist_ok=True)
331
+
332
+ # matplotlib νƒ€μž„λΌμΈ κ·Έλ¦Ό 생성
333
+ fig, axes = plt.subplots(3, 1, figsize=(7, 10), sharex=True)
334
+
335
+ axes[0].plot(times, ps, linewidth=1.2)
336
+ axes[0].set_ylabel("p_surprise")
337
+ axes[0].set_title("Surprise Probability")
338
+
339
+ axes[1].plot(times, bpm, linewidth=1.2)
340
+ axes[1].set_ylabel("BPM")
341
+ axes[1].set_title("Heart-rate (smoothed)")
342
+
343
+ axes[2].plot(times, combined, linewidth=1.2)
344
+ axes[2].set_ylabel("combined")
345
+ axes[2].set_xlabel("Time (s)")
346
+ axes[2].set_title("Combined Score")
347
+
348
+ for ax in axes:
349
+ ax.grid(True, alpha=0.3)
350
+
351
+ fig.tight_layout()
352
+
353
+ timeline_path = os.path.join(reports_dir, "timeline.png")
354
+ fig.savefig(timeline_path, bbox_inches="tight")
355
+ plt.close(fig)
356
+
357
+ # Top3 이미지 경둜
358
+ img_paths = []
359
+ for ev in top3:
360
+ frame_rel = ev.get("frame_file", "")
361
+ frame_abs = os.path.join(session_dir, frame_rel)
362
+ if os.path.exists(frame_abs):
363
+ img_paths.append(frame_abs)
364
+ else:
365
+ img_paths.append(None)
366
+
367
+ pdf_path = os.path.join(reports_dir, "surprise_report.pdf")
368
+
369
+ pdf = FPDF()
370
+ pdf.add_page()
371
+
372
+ # Title
373
+ pdf.set_font("Arial", "B", 16)
374
+ pdf.cell(0, 10, "Surprise Analyzer Report", ln=1)
375
+
376
+ # Alpha/Beta/Baseline
377
+ pdf.set_font("Arial", "", 11)
378
+ pdf.cell(0, 8, f"Alpha (expression weight): {alpha:.2f}", ln=1)
379
+ pdf.cell(0, 8, f"Beta (heart-rate weight): {beta:.2f}", ln=1)
380
+ pdf.cell(0, 8, f"Baseline BPM (median of valid BPM): {baseline:.2f}", ln=1)
381
+ pdf.ln(2)
382
+
383
+ # Meta 정보
384
+ if meta:
385
+ pdf.set_font("Arial", "B", 12)
386
+ pdf.cell(0, 8, "Session Meta", ln=1)
387
+ pdf.set_font("Arial", "", 11)
388
+ for k, v in meta.items():
389
+ pdf.cell(0, 6, f"- {k}: {v}", ln=1)
390
+ pdf.ln(2)
391
+
392
+ # Top3 summary
393
+ pdf.set_font("Arial", "B", 12)
394
+ pdf.cell(0, 8, "Top 3 Surprise Moments", ln=1)
395
+ pdf.set_font("Arial", "", 11)
396
+ if len(top3) == 0:
397
+ pdf.cell(0, 6, "No surprise events detected.", ln=1)
398
+ else:
399
+ for i, ev in enumerate(top3):
400
+ t = ev["time"]
401
+ ps_val = ev["p_surprise"]
402
+ bpm_val = ev["bpm"]
403
+ cs_val = ev["combined_score"]
404
+ pdf.multi_cell(
405
+ 0, 6,
406
+ f"#{i+1} time = {t:.2f}s, "
407
+ f"p_surprise = {ps_val:.4f}, "
408
+ f"BPM = {bpm_val if bpm_val is not None else 'None'}, "
409
+ f"combined = {cs_val:.4f}"
410
+ )
411
+ pdf.ln(2)
412
+
413
+ # Timeline image
414
+ pdf.set_font("Arial", "B", 12)
415
+ pdf.cell(0, 8, "Surprise Timeline", ln=1)
416
+ pdf.image(timeline_path, w=180)
417
+ pdf.ln(4)
418
+
419
+ # Top3 images
420
+ pdf.set_font("Arial", "B", 12)
421
+ pdf.cell(0, 8, "Top 3 Frames", ln=1)
422
+ pdf.set_font("Arial", "", 11)
423
+
424
+ for i, p in enumerate(img_paths):
425
+ if p is None:
426
+ continue
427
+ pdf.cell(0, 6, f"Top {i+1}", ln=1)
428
+ pdf.image(p, w=80)
429
+ pdf.ln(3)
430
+
431
+ pdf.output(pdf_path)
432
+ return pdf_path
433
+
434
+
435
+ # =========================
436
+ # 7. Gradio 메인 처리 ν•¨μˆ˜
437
+ # =========================
438
+ def process(zip_file, alpha):
439
+ """
440
+ Gradioμ—μ„œ ν˜ΈμΆœλ˜λŠ” 메인 ν•¨μˆ˜.
441
+ zip_file: μ—…λ‘œλ“œλœ ZIP 파일 경둜
442
+ alpha: μŠ¬λΌμ΄λ”λ‘œ 받은 Ξ±κ°’
443
+ """
444
+ if zip_file is None:
445
+ return (
446
+ "Please upload a session ZIP created by collector.py.",
447
+ None, None, None,
448
+ None, None,
449
+ None
450
+ )
451
+
452
+ beta = 1.0 - alpha
453
+
454
+ # 1) μ„Έμ…˜ λ‘œλ“œ
455
+ samples, events, meta, session_dir = load_session(zip_file)
456
+
457
+ # 2) νƒ€μž„λΌμΈ 뢄석 (BPM smoothing + combined)
458
+ times, ps, bpm_smooth, combined, baseline = analyze_timeline(samples, alpha, ma_window=5)
459
+
460
+ # 3) BPM κΈ‰μƒμŠΉ ꡬ간 탐지
461
+ spike_times = detect_bpm_spikes(times, bpm_smooth, baseline, spike_delta=10.0, min_gap_sec=1.0)
462
+
463
+ # 4) Top3 이벀트 μ„ μ • (μ΅œμ†Œ 1초 간격)
464
+ top3 = pick_top3_events(events, times, combined, min_gap_sec=1.0)
465
+
466
+ # 5) Plotly νƒ€μž„λΌμΈ + Heatmap 생성
467
+ timeline_fig = build_timeline_plot(times, ps, bpm_smooth, combined, top3, spike_times)
468
+ heatmap_fig = build_heatmap(times, combined, bins=50)
469
+
470
+ # 6) PDF 생성
471
+ pdf_path = create_pdf(session_dir, meta, top3, times, ps, bpm_smooth, combined, alpha, beta, baseline)
472
+
473
+ # 7) Top3 이미지 λ‘œλ”©
474
+ img1 = img2 = img3 = None
475
+ if len(top3) >= 1:
476
+ p1 = os.path.join(session_dir, top3[0]["frame_file"])
477
+ if os.path.exists(p1):
478
+ img1 = Image.open(p1)
479
+ if len(top3) >= 2:
480
+ p2 = os.path.join(session_dir, top3[1]["frame_file"])
481
+ if os.path.exists(p2):
482
+ img2 = Image.open(p2)
483
+ if len(top3) >= 3:
484
+ p3 = os.path.join(session_dir, top3[2]["frame_file"])
485
+ if os.path.exists(p3):
486
+ img3 = Image.open(p3)
487
+
488
+ # 8) Summary ν…μŠ€νŠΈ
489
+ lines = [
490
+ f"Alpha (expression weight) = {alpha:.2f}",
491
+ f"Beta (heart-rate weight) = {beta:.2f}",
492
+ f"Baseline BPM (median of valid BPM) = {baseline:.2f}",
493
+ f"Number of samples = {len(samples)}",
494
+ f"Number of raw events (expression-based) = {len(events)}",
495
+ f"Number of detected BPM spikes = {len(spike_times)}",
496
+ ""
497
+ ]
498
+
499
+ if spike_times:
500
+ lines.append("BPM spike times (s):")
501
+ lines.append(", ".join([f"{t:.2f}" for t in spike_times]))
502
+ lines.append("")
503
+
504
+ if len(top3) == 0:
505
+ lines.append("No surprise events detected above the current settings.")
506
+ else:
507
+ lines.append("Top 3 surprise moments (time, p_surprise, BPM, combined_score):")
508
+ for i, ev in enumerate(top3):
509
+ t = ev["time"]
510
+ ps_val = ev["p_surprise"]
511
+ bpm_val = ev["bpm"]
512
+ cs_val = ev["combined_score"]
513
+ lines.append(
514
+ f"#{i+1} time = {t:.2f}s, "
515
+ f"p_surprise = {ps_val:.4f}, "
516
+ f"BPM = {bpm_val if bpm_val is not None else 'None'}, "
517
+ f"combined_score = {cs_val:.4f}"
518
+ )
519
+
520
+ summary = "\n".join(lines)
521
+
522
+ return summary, img1, img2, img3, timeline_fig, heatmap_fig, pdf_path
523
+
524
+
525
+ # =========================
526
+ # 8. Gradio UI
527
+ # =========================
528
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")) as demo:
529
+ gr.Markdown(
530
+ """
531
+ # 🎭 Surprise Analyzer (Expression + Heart-rate, v2)
532
+
533
+ 이 웹앱은 **둜컬 μˆ˜μ§‘κΈ°(collector.py)** 둜 λ…Ήν™”ν•œ μ„Έμ…˜ ZIP을 μ—…λ‘œλ“œν•˜λ©΄,
534
+ ν‘œμ • 기반 λ†€λžŒ ν™•λ₯ (`p_surprise`)κ³Ό 심박(`BPM`)을 κ²°ν•©ν•΄μ„œ **Top 3 λ†€λž€ μˆœκ°„**을 μ°Ύμ•„μ€λ‹ˆλ‹€.
535
+
536
+ ### πŸ” 전체 νŒŒμ΄ν”„λΌμΈ
537
+ 1. λ‘œμ»¬μ—μ„œ μ›ΉμΊ  + 아두이노 심박 μ„Όμ„œλ‘œ 데이터 μˆ˜μ§‘ (`collector.py` μ‹€ν–‰)
538
+ 2. μƒμ„±λœ `session_YYYYMMDD_XXXXXX.zip` νŒŒμΌμ„ 이곳에 μ—…λ‘œλ“œ
539
+ 3. μ•„λž˜ μŠ¬λΌμ΄λ”λ‘œ **ν‘œμ • vs 심박 κ°€μ€‘μΉ˜(Ξ±, Ξ²)** 쑰절
540
+ 4. **νƒ€μž„λΌμΈ κ·Έλž˜ν”„ + Heatmap + Top3 μž₯λ©΄ + PDF 리포트** 확인
541
+
542
+ - βœ… BPM κ·Έλž˜ν”„μ— moving average smoothing 적용
543
+ - βœ… p_surprise / BPM / combined_scoreλ₯Ό **ν•˜λ‚˜μ˜ Plotly νƒ€μž„λΌμΈ**에 ν‘œμ‹œ
544
+ - βœ… Top3λŠ” μ„œλ‘œ **1초 이상 간격** μœ μ§€ν•˜λ„λ‘ 보정
545
+ - βœ… combined_score 기반 **λ†€λžŒ 강도 Heatmap** 제곡
546
+ - βœ… **심박 κΈ‰μƒμŠΉ ꡬ간 μžλ™ 탐지** ν›„ νƒ€μž„λΌμΈμ— ν‘œμ‹œ
547
+ ---
548
+ """
549
+ )
550
+
551
+ with gr.Row():
552
+ with gr.Column(scale=1):
553
+ zip_input = gr.File(
554
+ label="πŸ“‚ Upload session ZIP (collector.py output)",
555
+ type="filepath"
556
+ )
557
+ alpha_slider = gr.Slider(
558
+ minimum=0.0,
559
+ maximum=1.0,
560
+ value=0.6,
561
+ step=0.05,
562
+ label="Ξ±: Expression weight (Ξ² = 1 βˆ’ Ξ±, heart-rate weight)"
563
+ )
564
+ analyze_btn = gr.Button("πŸš€ Run Analysis", variant="primary")
565
+
566
+ with gr.Column(scale=2):
567
+ summary_box = gr.Textbox(
568
+ label="πŸ“„ Analysis Summary",
569
+ lines=14
570
+ )
571
+
572
+ gr.Markdown("### πŸ† Top 3 Surprise Frames")
573
+
574
+ with gr.Row():
575
+ img1 = gr.Image(label="Top 1")
576
+ img2 = gr.Image(label="Top 2")
577
+ img3 = gr.Image(label="Top 3")
578
+
579
+ gr.Markdown("### πŸ“Š Timelines & Heatmap")
580
+
581
+ timeline_plot = gr.Plot(label="Surprise / BPM / Combined Timeline")
582
+ heatmap_plot = gr.Plot(label="Surprise Intensity Heatmap")
583
+
584
+ gr.Markdown("### πŸ“₯ Download Report (PDF)")
585
+
586
+ pdf_file = gr.File(label="Download PDF Report")
587
+
588
+ analyze_btn.click(
589
+ fn=process,
590
+ inputs=[zip_input, alpha_slider],
591
+ outputs=[summary_box, img1, img2, img3, timeline_plot, heatmap_plot, pdf_file]
592
+ )
593
+
594
+ if __name__ == "__main__":
595
+ demo.launch()