sseo325 commited on
Commit
bc8f518
·
verified ·
1 Parent(s): cd6a611

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +399 -401
app.py CHANGED
@@ -1,401 +1,399 @@
1
- import os
2
- import time
3
- import cv2
4
- import numpy as np
5
- import tensorflow as tf
6
- import gradio as gr
7
- import plotly.graph_objects as go
8
- import matplotlib.pyplot as plt
9
- from fpdf import FPDF
10
- from PIL import Image
11
-
12
- # ===============================
13
- # 1. Load Model
14
- # ===============================
15
- MODEL_PATH = "fer_surprise_softmax.h5"
16
- model = tf.keras.models.load_model(MODEL_PATH, compile=False)
17
-
18
- IMG_SIZE = (96, 96)
19
- CLASS_NAMES = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"]
20
- SURPRISE_IDX = CLASS_NAMES.index("surprise")
21
-
22
- # ===============================
23
- # 2. Face Detector
24
- # ===============================
25
- face_cascade = cv2.CascadeClassifier(
26
- cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
27
- )
28
-
29
- # ===============================
30
- # 3. State Storage
31
- # ===============================
32
- events = [] # surprise events (for Top 3)
33
- surprise_history = [] # (time, p_surprise) for timeline
34
- start_time = None # stream start time
35
- MIN_EVENT_GAP = 1.0 # min seconds between events
36
-
37
- # Session stats
38
- frames_with_face = 0 # how many frames had a detected face
39
- max_p_surprise = 0.0 # maximum P(surprise) observed in the session
40
-
41
- # ===============================
42
- # 4. Utility: Time Formatting
43
- # ===============================
44
- def format_time(seconds: float) -> str:
45
- minutes = int(seconds // 60)
46
- sec = int(seconds % 60)
47
- return f"{minutes:02d}:{sec:02d}"
48
-
49
-
50
- # ===============================
51
- # 5. Real-time Frame Processing
52
- # ===============================
53
- def detect_surprise(frame, threshold):
54
-
55
- global events, start_time, surprise_history
56
- global frames_with_face, max_p_surprise
57
-
58
- if frame is None:
59
- stats_text = (
60
- "### Session Stats\n"
61
- "- Session duration: 00:00\n"
62
- f"- Current threshold: {threshold:.2f}\n"
63
- "- Frames with face detected: 0\n"
64
- "- Surprise events detected: 0\n"
65
- "- Max P(surprise): 0.00\n"
66
- )
67
- return None, {"Error": 1.0}, None, stats_text
68
-
69
- if start_time is None:
70
- start_time = time.time()
71
- surprise_history = []
72
- events = []
73
- frames_with_face = 0
74
- max_p_surprise = 0.0
75
-
76
- current_time = time.time() - start_time
77
-
78
- frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
79
- gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
80
-
81
- faces = face_cascade.detectMultiScale(gray, 1.1, 4)
82
-
83
- # 변경된 기본 라벨: 얼굴 미검출 시 조명/각도 안내
84
- label = "NO FACE - Try brighter lighting or adjust angle"
85
- color = (0, 255, 255)
86
- probs_dict = {}
87
-
88
- if len(faces) > 0:
89
- frames_with_face += 1
90
- x, y, w, h = sorted(faces, key=lambda r: r[2] * r[3], reverse=True)[0]
91
- roi = frame_bgr[y:y+h, x:x+w]
92
-
93
- rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
94
- resized = cv2.resize(rgb, IMG_SIZE)
95
- inp = resized.astype("float32") / 255.0
96
- inp = np.expand_dims(inp, axis=0)
97
-
98
- probs = model.predict(inp, verbose=0)[0]
99
- p_surprise = float(probs[SURPRISE_IDX])
100
-
101
- if p_surprise > max_p_surprise:
102
- max_p_surprise = p_surprise
103
-
104
- probs_dict = {
105
- cls: float(p) for cls, p in zip(CLASS_NAMES, probs)
106
- }
107
-
108
- surprise_history.append({
109
- "time": current_time,
110
- "score": p_surprise,
111
- })
112
-
113
- # -------- Top3 detection logic --------
114
- if p_surprise >= threshold:
115
- if len(events) == 0:
116
- events.append({
117
- "time": current_time,
118
- "score": p_surprise,
119
- "frame": frame.copy()
120
- })
121
- else:
122
- dt = current_time - events[-1]["time"]
123
- if dt > MIN_EVENT_GAP:
124
- events.append({
125
- "time": current_time,
126
- "score": p_surprise,
127
- "frame": frame.copy()
128
- })
129
- else:
130
- if p_surprise > events[-1]["score"]:
131
- events[-1]["time"] = current_time
132
- events[-1]["score"] = p_surprise
133
- events[-1]["frame"] = frame.copy()
134
-
135
- label = f"😲 SURPRISE (p={p_surprise:.2f})"
136
- color = (0, 255, 0)
137
-
138
- else:
139
- label = f"🙂 Not Surprise (p={p_surprise:.2f})"
140
- color = (0, 0, 255)
141
-
142
- # Draw bounding box
143
- cv2.rectangle(frame_bgr, (x, y), (x + w, y + h), color, 3)
144
-
145
- # -------- Label 위치: 왼쪽 아래 + 큰 글씨 --------
146
- h_img, w_img = frame_bgr.shape[:2]
147
- cv2.putText(
148
- frame_bgr,
149
- label,
150
- (10, h_img - 10),
151
- cv2.FONT_HERSHEY_SIMPLEX,
152
- 1.6,
153
- color,
154
- 3
155
- )
156
-
157
- out_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
158
-
159
- # Per-frame bar chart
160
- fig = go.Figure()
161
- if len(probs_dict) > 0:
162
- fig.add_trace(go.Bar(
163
- x=list(probs_dict.keys()),
164
- y=list(probs_dict.values()),
165
- marker_color="lightskyblue"
166
- ))
167
- fig.update_layout(
168
- title="Emotion Probability Distribution",
169
- yaxis=dict(range=[0, 1])
170
- )
171
-
172
- session_duration_str = format_time(current_time)
173
- stats_text = (
174
- "### Session Stats\n"
175
- f"- Session duration: {session_duration_str}\n"
176
- f"- Current threshold: {threshold:.2f}\n"
177
- f"- Frames with face detected: {frames_with_face}\n"
178
- f"- Surprise events detected: {len(events)}\n"
179
- f"- Max P(surprise): {max_p_surprise:.2f}\n"
180
- )
181
-
182
- return out_rgb, probs_dict, fig, stats_text
183
-
184
-
185
- # ===============================
186
- # 6. PDF Generation
187
- # ===============================
188
- def create_pdf(summary_text, top_images, timeline_fig):
189
- os.makedirs("reports", exist_ok=True)
190
- timestamp = int(time.time())
191
- pdf_path = os.path.join("reports", f"surprise_report_{timestamp}.pdf")
192
-
193
- timeline_path = os.path.join("reports", f"timeline_{timestamp}.png")
194
- timeline_fig.savefig(timeline_path, bbox_inches="tight")
195
-
196
- img_paths = []
197
- for i, img in enumerate(top_images):
198
- if img is None:
199
- img_paths.append(None)
200
- continue
201
- img_pil = Image.fromarray(img)
202
- img_path = os.path.join("reports", f"top{i+1}_{timestamp}.png")
203
- img_pil.save(img_path)
204
- img_paths.append(img_path)
205
-
206
- pdf = FPDF()
207
- pdf.add_page()
208
-
209
- pdf.set_font("Arial", "B", 16)
210
- pdf.cell(0, 10, "Real-Time Surprise Detector Report", ln=1)
211
-
212
- pdf.set_font("Arial", "", 11)
213
- pdf.multi_cell(0, 6, summary_text)
214
- pdf.ln(4)
215
-
216
- pdf.set_font("Arial", "B", 12)
217
- pdf.cell(0, 8, "Surprise Probability Timeline", ln=1)
218
- pdf.image(timeline_path, w=170)
219
- pdf.ln(4)
220
-
221
- pdf.set_font("Arial", "B", 12)
222
- pdf.cell(0, 8, "Top Surprise Frames", ln=1)
223
- pdf.set_font("Arial", "", 11)
224
-
225
- for i, path in enumerate(img_paths):
226
- if path is not None:
227
- pdf.cell(0, 6, f"Top {i+1}", ln=1)
228
- pdf.image(path, w=80)
229
- pdf.ln(2)
230
-
231
- pdf.output(pdf_path)
232
- return pdf_path
233
-
234
-
235
- # ===============================
236
- # 7. Summarize Results
237
- # ===============================
238
- def summarize_results():
239
-
240
- global events, start_time, surprise_history
241
- global frames_with_face, max_p_surprise
242
-
243
- if len(surprise_history) == 0:
244
- return "No data recorded.", None, None, None, None, None
245
-
246
- times = [h["time"] for h in surprise_history]
247
- scores = [h["score"] for h in surprise_history]
248
-
249
- fig, ax = plt.subplots()
250
- ax.plot(times, scores, marker="o", linewidth=1)
251
- ax.set_title("Surprise Probability Timeline")
252
- ax.set_xlabel("Time (s)")
253
- ax.set_ylabel("P(surprise)")
254
- ax.set_ylim(0, 1)
255
- ax.grid(True)
256
-
257
- top_images = [None, None, None]
258
- if len(events) == 0:
259
- summary_text = (
260
- "No surprise events detected above the current threshold.\n\n"
261
- "The timeline shows overall surprise probability over time."
262
- )
263
- img1 = img2 = img3 = None
264
-
265
- else:
266
- top3 = sorted(events, key=lambda x: x["score"], reverse=True)[:3]
267
-
268
- captions = []
269
- images = []
270
- top_times = []
271
- top_scores = []
272
-
273
- for i, e in enumerate(top3):
274
- formatted_time = format_time(e["time"])
275
- score = e["score"]
276
- captions.append(f"#{i+1} Time = {formatted_time} Score = {score:.2f}")
277
- images.append(e["frame"])
278
- top_times.append(e["time"])
279
- top_scores.append(score)
280
-
281
- summary_text = "Top 3 surprise moments:\n" + "\n".join(captions)
282
-
283
- markers = ["*", "^", "s"]
284
- colors = ["red", "darkorange", "gold"]
285
-
286
- for i, (t, s) in enumerate(zip(top_times, top_scores)):
287
- ax.scatter(t, s, color=colors[i], marker=markers[i], s=80, zorder=5)
288
-
289
- for i in range(3):
290
- if i < len(images):
291
- top_images[i] = images[i]
292
-
293
- img1, img2, img3 = top_images
294
-
295
- pdf_path = create_pdf(summary_text, top_images, fig)
296
-
297
- events = []
298
- start_time = None
299
- surprise_history = []
300
- frames_with_face = 0
301
- max_p_surprise = 0.0
302
-
303
- return summary_text, img1, img2, img3, fig, pdf_path
304
-
305
-
306
- # ===============================
307
- # 8. UI
308
- # ===============================
309
- try:
310
- custom_theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
311
- except:
312
- custom_theme = "soft"
313
-
314
- demo = gr.Blocks(theme=custom_theme)
315
-
316
- with demo:
317
-
318
- gr.Markdown(
319
- """
320
- # 🎭 Real-Time Surprise Detector
321
- ### A real-time facial reaction analysis system
322
- ##### Detects surprise reactions using facial emotion recognition and summarizes top 3 peak surprise moments.
323
-
324
- **How to use:**
325
- 1. Enable your webcam by clicking the feed area.
326
- 2. Watch your chosen video while keeping your face visible.
327
- 3. If many frames show **"NO FACE"**, try brighter lighting or adjust your face angle.
328
- 4. Click **“Show Top 3 Surprise Moments”** after stopping the stream.
329
- 5. Download the generated PDF if needed.
330
- ---
331
- """
332
- )
333
-
334
- with gr.Row():
335
- with gr.Column(scale=2):
336
-
337
- webcam = gr.Image(
338
- sources=["webcam"],
339
- type="numpy",
340
- label="Webcam Feed"
341
- )
342
- output_img = gr.Image(label="Detection Result")
343
-
344
- with gr.Column(scale=1):
345
- threshold = gr.Slider(
346
- minimum=0.0, maximum=1.0, value=0.1,
347
- step=0.01, label="Surprise Threshold"
348
- )
349
-
350
- gr.Markdown(
351
- """
352
- ### What is the Surprise Threshold?
353
-
354
- - Lower threshold → detects smaller reactions
355
- - Higher threshold → detects only strong surprise
356
- - **Default = 0.1**
357
-
358
- 👉 Try making a surprised face to adjust sensitivity.
359
- """
360
- )
361
-
362
- output_label = gr.Label(label="Softmax Probabilities")
363
- plot = gr.Plot(label="Emotion Probability (per frame)")
364
- stats_md = gr.Markdown("### Session Stats\nWaiting for stream...")
365
-
366
- webcam.stream(
367
- fn=detect_surprise,
368
- inputs=[webcam, threshold],
369
- outputs=[output_img, output_label, plot, stats_md],
370
- stream_every=0.1
371
- )
372
-
373
- gr.Markdown("---")
374
- gr.Markdown("## 🔍 Summary & Report")
375
-
376
- summarize_button = gr.Button("🎯 Show Top 3 Surprise Moments")
377
-
378
- summary_text = gr.Textbox(
379
- label="Top 3 Summary",
380
- lines=6,
381
- max_lines=10
382
- )
383
-
384
- with gr.Row():
385
- img1 = gr.Image(label="Top 1")
386
- img2 = gr.Image(label="Top 2")
387
- img3 = gr.Image(label="Top 3")
388
-
389
- timeline_plot = gr.Plot(label="Surprise Timeline")
390
- pdf_file = gr.File(label="Download PDF Report")
391
-
392
- summarize_button.click(
393
- fn=summarize_results,
394
- inputs=[],
395
- outputs=[summary_text, img1, img2, img3, timeline_plot, pdf_file]
396
- )
397
-
398
- if __name__ == "__main__":
399
- demo.launch()
400
-
401
- ::contentReference[oaicite:0]{index=0}
 
1
+ import os
2
+ import time
3
+ import cv2
4
+ import numpy as np
5
+ import tensorflow as tf
6
+ import gradio as gr
7
+ import plotly.graph_objects as go
8
+ import matplotlib.pyplot as plt
9
+ from fpdf import FPDF
10
+ from PIL import Image
11
+
12
+ # ===============================
13
+ # 1. Load Model
14
+ # ===============================
15
+ MODEL_PATH = "fer_surprise_softmax.h5"
16
+ model = tf.keras.models.load_model(MODEL_PATH, compile=False)
17
+
18
+ IMG_SIZE = (96, 96)
19
+ CLASS_NAMES = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"]
20
+ SURPRISE_IDX = CLASS_NAMES.index("surprise")
21
+
22
+ # ===============================
23
+ # 2. Face Detector
24
+ # ===============================
25
+ face_cascade = cv2.CascadeClassifier(
26
+ cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
27
+ )
28
+
29
+ # ===============================
30
+ # 3. State Storage
31
+ # ===============================
32
+ events = []
33
+ surprise_history = []
34
+ start_time = None
35
+ MIN_EVENT_GAP = 1.0
36
+
37
+ # Session stats
38
+ frames_with_face = 0
39
+ max_p_surprise = 0.0
40
+
41
+ # ===============================
42
+ # 4. Utility: Time Formatting
43
+ # ===============================
44
+ def format_time(seconds: float) -> str:
45
+ minutes = int(seconds // 60)
46
+ sec = int(seconds % 60)
47
+ return f"{minutes:02d}:{sec:02d}"
48
+
49
+
50
+ # ===============================
51
+ # 5. Real-time Frame Processing
52
+ # ===============================
53
+ def detect_surprise(frame, threshold):
54
+
55
+ global events, start_time, surprise_history
56
+ global frames_with_face, max_p_surprise
57
+
58
+ if frame is None:
59
+ stats_text = (
60
+ "### Session Stats\n"
61
+ "- Session duration: 00:00\n"
62
+ f"- Current threshold: {threshold:.2f}\n"
63
+ "- Frames with face detected: 0\n"
64
+ "- Surprise events detected: 0\n"
65
+ "- Max P(surprise): 0.00\n"
66
+ )
67
+ return None, {"Error": 1.0}, None, stats_text
68
+
69
+ if start_time is None:
70
+ start_time = time.time()
71
+ surprise_history = []
72
+ events = []
73
+ frames_with_face = 0
74
+ max_p_surprise = 0.0
75
+
76
+ current_time = time.time() - start_time
77
+
78
+ frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
79
+ gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
80
+
81
+ faces = face_cascade.detectMultiScale(gray, 1.1, 4)
82
+
83
+ # 변경된 기본 라벨: 얼굴 미검출 시 조명/각도 안내
84
+ label = "NO FACE - Try brighter lighting or adjust angle"
85
+ color = (0, 255, 255)
86
+ probs_dict = {}
87
+
88
+ if len(faces) > 0:
89
+ frames_with_face += 1
90
+ x, y, w, h = sorted(faces, key=lambda r: r[2] * r[3], reverse=True)[0]
91
+ roi = frame_bgr[y:y+h, x:x+w]
92
+
93
+ rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
94
+ resized = cv2.resize(rgb, IMG_SIZE)
95
+ inp = resized.astype("float32") / 255.0
96
+ inp = np.expand_dims(inp, axis=0)
97
+
98
+ probs = model.predict(inp, verbose=0)[0]
99
+ p_surprise = float(probs[SURPRISE_IDX])
100
+
101
+ if p_surprise > max_p_surprise:
102
+ max_p_surprise = p_surprise
103
+
104
+ probs_dict = {
105
+ cls: float(p) for cls, p in zip(CLASS_NAMES, probs)
106
+ }
107
+
108
+ surprise_history.append({
109
+ "time": current_time,
110
+ "score": p_surprise,
111
+ })
112
+
113
+ # -------- Top3 detection logic --------
114
+ if p_surprise >= threshold:
115
+ if len(events) == 0:
116
+ events.append({
117
+ "time": current_time,
118
+ "score": p_surprise,
119
+ "frame": frame.copy()
120
+ })
121
+ else:
122
+ dt = current_time - events[-1]["time"]
123
+ if dt > MIN_EVENT_GAP:
124
+ events.append({
125
+ "time": current_time,
126
+ "score": p_surprise,
127
+ "frame": frame.copy()
128
+ })
129
+ else:
130
+ if p_surprise > events[-1]["score"]:
131
+ events[-1]["time"] = current_time
132
+ events[-1]["score"] = p_surprise
133
+ events[-1]["frame"] = frame.copy()
134
+
135
+ label = f"😲 SURPRISE (p={p_surprise:.2f})"
136
+ color = (0, 255, 0)
137
+
138
+ else:
139
+ label = f"🙂 Not Surprise (p={p_surprise:.2f})"
140
+ color = (0, 0, 255)
141
+
142
+ # Draw bounding box
143
+ cv2.rectangle(frame_bgr, (x, y), (x + w, y + h), color, 3)
144
+
145
+ # -------- Label 위치: 왼쪽 아래 + 큰 글씨 --------
146
+ h_img, w_img = frame_bgr.shape[:2]
147
+ cv2.putText(
148
+ frame_bgr,
149
+ label,
150
+ (10, h_img - 10),
151
+ cv2.FONT_HERSHEY_SIMPLEX,
152
+ 1.6,
153
+ color,
154
+ 3
155
+ )
156
+
157
+ out_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
158
+
159
+ # Per-frame bar chart
160
+ fig = go.Figure()
161
+ if len(probs_dict) > 0:
162
+ fig.add_trace(go.Bar(
163
+ x=list(probs_dict.keys()),
164
+ y=list(probs_dict.values()),
165
+ marker_color="lightskyblue"
166
+ ))
167
+ fig.update_layout(
168
+ title="Emotion Probability Distribution",
169
+ yaxis=dict(range=[0, 1])
170
+ )
171
+
172
+ session_duration_str = format_time(current_time)
173
+ stats_text = (
174
+ "### Session Stats\n"
175
+ f"- Session duration: {session_duration_str}\n"
176
+ f"- Current threshold: {threshold:.2f}\n"
177
+ f"- Frames with face detected: {frames_with_face}\n"
178
+ f"- Surprise events detected: {len(events)}\n"
179
+ f"- Max P(surprise): {max_p_surprise:.2f}\n"
180
+ )
181
+
182
+ return out_rgb, probs_dict, fig, stats_text
183
+
184
+
185
+ # ===============================
186
+ # 6. PDF Generation
187
+ # ===============================
188
+ def create_pdf(summary_text, top_images, timeline_fig):
189
+ os.makedirs("reports", exist_ok=True)
190
+ timestamp = int(time.time())
191
+ pdf_path = os.path.join("reports", f"surprise_report_{timestamp}.pdf")
192
+
193
+ timeline_path = os.path.join("reports", f"timeline_{timestamp}.png")
194
+ timeline_fig.savefig(timeline_path, bbox_inches="tight")
195
+
196
+ img_paths = []
197
+ for i, img in enumerate(top_images):
198
+ if img is None:
199
+ img_paths.append(None)
200
+ continue
201
+ img_pil = Image.fromarray(img)
202
+ img_path = os.path.join("reports", f"top{i+1}_{timestamp}.png")
203
+ img_pil.save(img_path)
204
+ img_paths.append(img_path)
205
+
206
+ pdf = FPDF()
207
+ pdf.add_page()
208
+
209
+ pdf.set_font("Arial", "B", 16)
210
+ pdf.cell(0, 10, "Real-Time Surprise Detector Report", ln=1)
211
+
212
+ pdf.set_font("Arial", "", 11)
213
+ pdf.multi_cell(0, 6, summary_text)
214
+ pdf.ln(4)
215
+
216
+ pdf.set_font("Arial", "B", 12)
217
+ pdf.cell(0, 8, "Surprise Probability Timeline", ln=1)
218
+ pdf.image(timeline_path, w=170)
219
+ pdf.ln(4)
220
+
221
+ pdf.set_font("Arial", "B", 12)
222
+ pdf.cell(0, 8, "Top Surprise Frames", ln=1)
223
+ pdf.set_font("Arial", "", 11)
224
+
225
+ for i, path in enumerate(img_paths):
226
+ if path is not None:
227
+ pdf.cell(0, 6, f"Top {i+1}", ln=1)
228
+ pdf.image(path, w=80)
229
+ pdf.ln(2)
230
+
231
+ pdf.output(pdf_path)
232
+ return pdf_path
233
+
234
+
235
+ # ===============================
236
+ # 7. Summarize Results
237
+ # ===============================
238
+ def summarize_results():
239
+
240
+ global events, start_time, surprise_history
241
+ global frames_with_face, max_p_surprise
242
+
243
+ if len(surprise_history) == 0:
244
+ return "No data recorded.", None, None, None, None, None
245
+
246
+ times = [h["time"] for h in surprise_history]
247
+ scores = [h["score"] for h in surprise_history]
248
+
249
+ fig, ax = plt.subplots()
250
+ ax.plot(times, scores, marker="o", linewidth=1)
251
+ ax.set_title("Surprise Probability Timeline")
252
+ ax.set_xlabel("Time (s)")
253
+ ax.set_ylabel("P(surprise)")
254
+ ax.set_ylim(0, 1)
255
+ ax.grid(True)
256
+
257
+ top_images = [None, None, None]
258
+ if len(events) == 0:
259
+ summary_text = (
260
+ "No surprise events detected above the current threshold.\n\n"
261
+ "The timeline shows overall surprise probability over time."
262
+ )
263
+ img1 = img2 = img3 = None
264
+
265
+ else:
266
+ top3 = sorted(events, key=lambda x: x["score"], reverse=True)[:3]
267
+
268
+ captions = []
269
+ images = []
270
+ top_times = []
271
+ top_scores = []
272
+
273
+ for i, e in enumerate(top3):
274
+ formatted_time = format_time(e["time"])
275
+ score = e["score"]
276
+ captions.append(f"#{i+1} Time = {formatted_time} Score = {score:.2f}")
277
+ images.append(e["frame"])
278
+ top_times.append(e["time"])
279
+ top_scores.append(score)
280
+
281
+ summary_text = "Top 3 surprise moments:\n" + "\n".join(captions)
282
+
283
+ markers = ["*", "^", "s"]
284
+ colors = ["red", "darkorange", "gold"]
285
+
286
+ for i, (t, s) in enumerate(zip(top_times, top_scores)):
287
+ ax.scatter(t, s, color=colors[i], marker=markers[i], s=80, zorder=5)
288
+
289
+ for i in range(3):
290
+ if i < len(images):
291
+ top_images[i] = images[i]
292
+
293
+ img1, img2, img3 = top_images
294
+
295
+ pdf_path = create_pdf(summary_text, top_images, fig)
296
+
297
+ events = []
298
+ start_time = None
299
+ surprise_history = []
300
+ frames_with_face = 0
301
+ max_p_surprise = 0.0
302
+
303
+ return summary_text, img1, img2, img3, fig, pdf_path
304
+
305
+
306
+ # ===============================
307
+ # 8. UI
308
+ # ===============================
309
+ try:
310
+ custom_theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
311
+ except:
312
+ custom_theme = "soft"
313
+
314
+ demo = gr.Blocks(theme=custom_theme)
315
+
316
+ with demo:
317
+
318
+ gr.Markdown(
319
+ """
320
+ # 🎭 Real-Time Surprise Detector
321
+ ### A real-time facial reaction analysis system
322
+ ##### Detects surprise reactions using facial emotion recognition and summarizes top 3 peak surprise moments.
323
+
324
+ **How to use:**
325
+ 1. Enable your webcam by clicking the feed area.
326
+ 2. Watch your chosen video while keeping your face visible.
327
+ 3. If many frames show **"NO FACE"**, try brighter lighting or adjust your face angle.
328
+ 4. Click **“Show Top 3 Surprise Moments”** after stopping the stream.
329
+ 5. Download the generated PDF if needed.
330
+ ---
331
+ """
332
+ )
333
+
334
+ with gr.Row():
335
+ with gr.Column(scale=2):
336
+
337
+ webcam = gr.Image(
338
+ sources=["webcam"],
339
+ type="numpy",
340
+ label="Webcam Feed"
341
+ )
342
+ output_img = gr.Image(label="Detection Result")
343
+
344
+ with gr.Column(scale=1):
345
+ threshold = gr.Slider(
346
+ minimum=0.0, maximum=1.0, value=0.1,
347
+ step=0.01, label="Surprise Threshold"
348
+ )
349
+
350
+ gr.Markdown(
351
+ """
352
+ ### What is the Surprise Threshold?
353
+
354
+ - Lower threshold → detects smaller reactions
355
+ - Higher threshold → detects only strong surprise
356
+ - **Default = 0.1**
357
+
358
+ 👉 Try making a surprised face to adjust sensitivity.
359
+ """
360
+ )
361
+
362
+ output_label = gr.Label(label="Softmax Probabilities")
363
+ plot = gr.Plot(label="Emotion Probability (per frame)")
364
+ stats_md = gr.Markdown("### Session Stats\nWaiting for stream...")
365
+
366
+ webcam.stream(
367
+ fn=detect_surprise,
368
+ inputs=[webcam, threshold],
369
+ outputs=[output_img, output_label, plot, stats_md],
370
+ stream_every=0.1
371
+ )
372
+
373
+ gr.Markdown("---")
374
+ gr.Markdown("## 🔍 Summary & Report")
375
+
376
+ summarize_button = gr.Button("🎯 Show Top 3 Surprise Moments")
377
+
378
+ summary_text = gr.Textbox(
379
+ label="Top 3 Summary",
380
+ lines=6,
381
+ max_lines=10
382
+ )
383
+
384
+ with gr.Row():
385
+ img1 = gr.Image(label="Top 1")
386
+ img2 = gr.Image(label="Top 2")
387
+ img3 = gr.Image(label="Top 3")
388
+
389
+ timeline_plot = gr.Plot(label="Surprise Timeline")
390
+ pdf_file = gr.File(label="Download PDF Report")
391
+
392
+ summarize_button.click(
393
+ fn=summarize_results,
394
+ inputs=[],
395
+ outputs=[summary_text, img1, img2, img3, timeline_plot, pdf_file]
396
+ )
397
+
398
+ if __name__ == "__main__":
399
+ demo.launch()