Shouvik599 commited on
Commit
8affea8
·
1 Parent(s): c87c002

Fix 2 for emotion data capture

Browse files
Files changed (4) hide show
  1. app.py +21 -16
  2. models/facial_emotion.py +1 -1
  3. templates/facial.html +235 -198
  4. templates/results.html +31 -14
app.py CHANGED
@@ -174,7 +174,7 @@ def analyze_frame_api():
174
  data = request.get_json()
175
  image_data = data.get('image')
176
  if not image_data:
177
- return jsonify({'error': 'No image provided'}), 400
178
 
179
  # Remove header "data:image/jpeg;base64,"
180
  header, encoded = image_data.split(",", 1)
@@ -185,29 +185,34 @@ def analyze_frame_api():
185
  # Analyze the frame
186
  dominant_emotion, emotion_score, _ = facial_emotion.analyze_frame(frame)
187
 
188
- if dominant_emotion and dominant_emotion != "Uncertain":
189
- return jsonify({
190
- 'detected': True,
191
- 'emotion': str(dominant_emotion),
192
- 'score': float(round(emotion_score, 2))
193
- })
194
- elif dominant_emotion == "Uncertain":
195
  return jsonify({
196
  'detected': False,
197
- 'emotion': 'Uncertain',
198
- 'score': float(round(emotion_score, 2)) if emotion_score else 0
199
  })
200
- else:
 
 
201
  return jsonify({
202
  'detected': False,
203
- 'emotion': 'No face detected',
204
- 'score': 0
205
  })
 
 
 
 
 
 
 
206
 
207
  except Exception as e:
208
  logging.error("Error in /api/analyze-frame: %s", e)
209
- return jsonify({'error': 'Emotion analysis failed'}), 500
210
-
211
  @app.route('/api/get-emotions', methods=['GET'])
212
  def get_emotions():
213
  """This endpoint is no longer the source of truth.
@@ -745,5 +750,5 @@ if __name__ == '__main__':
745
  # Start the Flask server
746
  # For production (Render), disable debug and use dynamic port
747
  port = int(os.environ.get('PORT', 7860))
748
- debug_mode = os.environ.get('FLASK_ENV') == 'development'
749
  app.run(debug=debug_mode, host="0.0.0.0", port=port)
 
174
  data = request.get_json()
175
  image_data = data.get('image')
176
  if not image_data:
177
+ return jsonify({'detected': False, 'emotion': 'No image provided', 'score': 0}), 400
178
 
179
  # Remove header "data:image/jpeg;base64,"
180
  header, encoded = image_data.split(",", 1)
 
185
  # Analyze the frame
186
  dominant_emotion, emotion_score, _ = facial_emotion.analyze_frame(frame)
187
 
188
+ # Build response based on what was detected
189
+ if dominant_emotion is None:
190
+ # No face detected at all
 
 
 
 
191
  return jsonify({
192
  'detected': False,
193
+ 'emotion': 'No face detected',
194
+ 'score': 0
195
  })
196
+
197
+ if dominant_emotion == "Uncertain":
198
+ # Face detected but below confidence threshold
199
  return jsonify({
200
  'detected': False,
201
+ 'emotion': 'Uncertain',
202
+ 'score': round(float(emotion_score), 2) if emotion_score else 0
203
  })
204
+
205
+ # Confident emotion detected
206
+ return jsonify({
207
+ 'detected': True,
208
+ 'emotion': str(dominant_emotion),
209
+ 'score': round(float(emotion_score), 2)
210
+ })
211
 
212
  except Exception as e:
213
  logging.error("Error in /api/analyze-frame: %s", e)
214
+ return jsonify({'detected': False, 'emotion': 'Error', 'score': 0}), 500
215
+
216
  @app.route('/api/get-emotions', methods=['GET'])
217
  def get_emotions():
218
  """This endpoint is no longer the source of truth.
 
750
  # Start the Flask server
751
  # For production (Render), disable debug and use dynamic port
752
  port = int(os.environ.get('PORT', 7860))
753
+ debug_mode = os.environ.get('FLASK_ENV') == 'development'
754
  app.run(debug=debug_mode, host="0.0.0.0", port=port)
models/facial_emotion.py CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b6dcd1f23c0722f061476ce9464c69dbd9b77a5649a28ec7959a4049c471375c
3
  size 3076
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b7ccbfc46cee4bfd13160d2a7933589ad80b42b33a690c8e7fa8b484553e393
3
  size 3076
templates/facial.html CHANGED
@@ -1,5 +1,6 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -14,15 +15,18 @@
14
  body {
15
  font-family: 'Inter', sans-serif;
16
  }
 
17
  .btn-gradient {
18
  background-image: linear-gradient(to right, #6EE7B7, #3B82F6, #9333EA);
19
  transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
20
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
21
  }
 
22
  .btn-gradient:hover {
23
  transform: scale(1.05);
24
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
25
  }
 
26
  .video-container {
27
  position: relative;
28
  width: 100%;
@@ -34,18 +38,21 @@
34
  border: 3px solid #6EE7B7;
35
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
36
  }
 
37
  #webcam-video {
38
  display: none;
39
  width: 100%;
40
  height: 100%;
41
  object-fit: cover;
42
  }
 
43
  #placeholder-img {
44
  display: block;
45
  width: 100%;
46
  height: 100%;
47
  object-fit: cover;
48
  }
 
49
  #analysis-status {
50
  position: absolute;
51
  top: 1rem;
@@ -57,6 +64,7 @@
57
  font-weight: bold;
58
  display: none;
59
  }
 
60
  /* Styling for disabled buttons */
61
  .disabled-btn {
62
  background-image: none;
@@ -66,10 +74,12 @@
66
  transform: none;
67
  box-shadow: none;
68
  }
 
69
  .disabled-btn:hover {
70
  transform: none;
71
  box-shadow: none;
72
  }
 
73
  .loader {
74
  border: 4px solid #f3f3f3;
75
  border-top: 4px solid #3B82F6;
@@ -78,19 +88,27 @@
78
  height: 24px;
79
  animation: spin 1s linear infinite;
80
  }
 
81
  @keyframes spin {
82
- 0% { transform: rotate(0deg); }
83
- 100% { transform: rotate(360deg); }
 
 
 
 
 
84
  }
85
  </style>
86
  </head>
 
87
  <body class="bg-gray-900 text-gray-200">
88
 
89
  <div class="min-h-screen flex flex-col items-center justify-center p-4 md:p-8">
90
 
91
  <!-- Header -->
92
  <header class="flex items-center justify-center mb-8">
93
- <svg class="h-10 w-10 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
94
  <path d="M12 21.5c-4.4 0-8-3.6-8-8 0-4.4 3.6-8 8-8s8 3.6 8 8c0 4.4-3.6 8-8 8z" />
95
  <path d="M12 10a2 2 0 100 4 2 2 0 000-4z" />
96
  <path d="M16 16l-1 1" />
@@ -98,7 +116,8 @@
98
  <path d="M16 8l1-1" />
99
  <path d="M8 8l-1-1" />
100
  </svg>
101
- <h1 class="text-3xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-teal-400 via-blue-500 to-purple-600 ml-4">
 
102
  ShantiView
103
  </h1>
104
  </header>
@@ -109,34 +128,45 @@
109
  Facial Emotion Analysis
110
  </h2>
111
  <p id="instruction-message" class="text-base md:text-lg mb-8 text-gray-400 leading-relaxed">
112
- Allow access to your webcam to begin the analysis. The "Proceed" button will be enabled once the analysis is complete.
 
113
  </p>
114
 
115
  <!-- Video Feed Section -->
116
  <div class="video-container mx-auto mb-4" style="position:relative;">
117
  <!-- This video element is already in the DOM, so no need to append it via JS -->
118
  <video id="webcam-video" autoplay playsinline></video>
119
- <img id="placeholder-img" src="https://placehold.co/900x506/1f2937/d1d5db?text=Webcam+Feed" alt="Webcam Video Placeholder">
 
120
  <div id="analysis-status" class="text-lg"></div>
121
  <!-- Progress Bar -->
122
  <div class="w-full bg-gray-700 rounded-full h-2.5 mt-4">
123
- <div id="progress-bar" class="bg-teal-500 h-2.5 rounded-full transition-all duration-300 ease-in-out" style="width: 0%;"></div>
 
 
124
  </div>
125
  </div>
126
 
127
  <!-- Control Buttons -->
128
  <div class="flex justify-center flex-wrap gap-4">
129
- <button id="start-btn" class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 bg-teal-500 hover:bg-teal-600">
 
130
  <span id="start-btn-text">Start</span>
131
  <div id="start-btn-loader" class="hidden loader"></div>
132
  </button>
133
- <button id="stop-btn" class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 bg-red-500 hover:bg-red-600 disabled-btn" disabled>
 
 
134
  Stop
135
  </button>
136
- <button id="proceed-btn" class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 btn-gradient disabled-btn" disabled>
 
 
137
  Proceed
138
  </button>
139
- <button id="start-over-btn" class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 bg-gray-600 hover:bg-gray-700 disabled-btn" disabled>
 
 
140
  Start Over
141
  </button>
142
  </div>
@@ -144,211 +174,218 @@
144
  </div>
145
 
146
  <script>
147
- // Get references to all the important DOM elements
148
- const webcamVideo = document.getElementById('webcam-video');
149
- const placeholderImg = document.getElementById('placeholder-img');
150
- const startBtn = document.getElementById('start-btn');
151
- const stopBtn = document.getElementById('stop-btn');
152
- const proceedBtn = document.getElementById('proceed-btn');
153
- const startOverBtn = document.getElementById('start-over-btn');
154
- const analysisStatus = document.getElementById('analysis-status');
155
- const progressBar = document.getElementById('progress-bar');
156
-
157
- // Create an off-screen canvas for capturing frames
158
- const canvas = document.createElement('canvas');
159
- canvas.width = 400;
160
- canvas.height = 300;
161
-
162
- // --- CLIENT-SIDE STATE ---
163
- // This is the key fix: emotions are stored HERE, not on the server session.
164
- let stream = null;
165
- let intervalId = null;
166
- let emotionResults = []; // Array to store captured emotion objects
167
- let maxEmotions = 15;
168
- let lastEmotion = null;
169
- let isAnalyzing = false; // Prevent overlapping requests
170
-
171
- // Function to update the status message on the screen
172
- function setStatus(msg, color) {
173
- analysisStatus.textContent = msg;
174
- analysisStatus.style.display = 'block';
175
- analysisStatus.style.color = color || 'white';
176
- }
177
-
178
- // Main function to start the webcam and begin analysis
179
- async function startWebcam() {
180
- try {
181
- // Request access to the user's video camera
182
- stream = await navigator.mediaDevices.getUserMedia({ video: true });
183
-
184
- // Assign the stream to the video element and make it visible
185
- webcamVideo.srcObject = stream;
186
- webcamVideo.style.display = 'block';
187
- placeholderImg.style.display = 'none';
188
-
189
- // Reset client-side state
190
- emotionResults = [];
191
- lastEmotion = null;
192
- isAnalyzing = false;
193
- progressBar.style.width = '0%';
194
- proceedBtn.disabled = true;
195
- stopBtn.disabled = false;
196
- startBtn.disabled = true;
197
- startOverBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- setStatus('Capturing emotions...', '#38bdf8');
 
 
 
 
 
 
 
 
 
 
200
 
201
- // Start capturing frames every 1.5 seconds (slightly slower to avoid overlap)
202
- intervalId = setInterval(captureAndSendFrame, 1500);
 
203
 
204
- } catch (e) {
205
- console.error("Webcam access error:", e);
206
- setStatus('Could not access webcam. Please allow permissions.', '#f87171');
207
  startBtn.disabled = false;
208
- }
209
- }
210
 
211
- // Function to stop the webcam stream
212
- function stopWebcam() {
213
- if (intervalId) {
214
- clearInterval(intervalId);
215
- intervalId = null;
216
- }
217
- if (stream) {
218
- stream.getTracks().forEach(track => track.stop());
219
- webcamVideo.srcObject = null;
220
- stream = null;
221
  }
222
 
223
- // Update UI to show the placeholder again
224
- webcamVideo.style.display = 'none';
225
- placeholderImg.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
- // Update button states
228
- setStatus('Webcam stopped.', '#fbbf24');
229
- stopBtn.disabled = true;
230
- startBtn.disabled = false;
231
 
232
- // Enable proceed if we captured at least one emotion
233
- if (emotionResults.length > 0) {
234
- proceedBtn.disabled = false;
235
- }
236
- }
237
-
238
- // Function to capture a frame from the video and send it for analysis
239
- async function captureAndSendFrame() {
240
- // Guard: prevent overlapping requests
241
- if (isAnalyzing) {
242
- console.log("Skipping frame - previous analysis still in progress");
243
- return;
244
- }
245
- if (!webcamVideo.srcObject) return;
246
- // Check if already done
247
- if (emotionResults.length >= maxEmotions) return;
248
-
249
- isAnalyzing = true;
250
-
251
- const context = canvas.getContext('2d');
252
- context.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
253
- const dataUrl = canvas.toDataURL('image/jpeg');
254
-
255
- try {
256
- const resp = await fetch('/api/analyze-frame', {
257
- method: 'POST',
258
- headers: { 'Content-Type': 'application/json' },
259
- body: JSON.stringify({ image: dataUrl }),
260
- credentials: 'include'
261
- });
262
-
263
- if (!resp.ok) {
264
- throw new Error(`API error: ${resp.statusText}`);
265
- }
266
 
267
- const data = await resp.json();
268
- lastEmotion = data.emotion;
269
 
270
- // --- KEY CHANGE: Accumulate results on the CLIENT ---
271
- if (data.detected && data.emotion && data.emotion !== "Uncertain" && data.emotion !== "No face detected") {
272
- emotionResults.push({
273
- emotion: data.emotion,
274
- score: data.score
275
- });
276
 
277
- // Keep only the last maxEmotions
278
- if (emotionResults.length > maxEmotions) {
279
- emotionResults = emotionResults.slice(-maxEmotions);
280
- }
 
 
 
281
 
282
- console.log(`✅ Emotion captured: ${data.emotion} (${data.score}). Total: ${emotionResults.length}/${maxEmotions}`);
283
- } else {
284
- console.log(`⏭️ Skipped: ${lastEmotion}. Count remains: ${emotionResults.length}/${maxEmotions}`);
285
- }
286
 
287
- // Update UI
288
- setStatus(`Captured ${emotionResults.length}/${maxEmotions} emotions... Last: ${lastEmotion}`, '#a78bfa');
 
 
289
 
290
- // Update progress bar
291
- const percent = (emotionResults.length / maxEmotions) * 100;
292
- progressBar.style.width = percent + '%';
293
 
294
- // Check if analysis is complete
295
- if (emotionResults.length >= maxEmotions) {
296
- stopWebcam();
297
- proceedBtn.disabled = false;
298
- setStatus(`Analysis complete! Captured ${emotionResults.length} emotions. You may proceed.`, '#10b981');
299
 
300
- // Store results in localStorage for the next page to use
301
- localStorage.setItem('facialEmotionResults', JSON.stringify(emotionResults));
302
- console.log("📦 Emotion results saved to localStorage:", emotionResults);
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
304
- } catch (e) {
305
- console.error("Error during frame analysis:", e);
306
- setStatus('Error detecting emotion. Retrying...', '#f87171');
307
- } finally {
308
- isAnalyzing = false;
309
  }
310
- }
311
-
312
- // Reset facial emotions analysis (client-side + server-side cleanup)
313
- async function resetFacialEmotions() {
314
- emotionResults = [];
315
- localStorage.removeItem('facialEmotionResults');
316
- try {
317
- await fetch('/api/reset-emotions', { method: 'POST', credentials: 'include' });
318
- } catch (e) {
319
- console.log("Server reset call failed (non-critical):", e);
320
  }
321
- }
322
-
323
- // Event listeners for the control buttons
324
- startBtn.addEventListener('click', async () => {
325
- await resetFacialEmotions();
326
- startWebcam();
327
- });
328
- stopBtn.addEventListener('click', stopWebcam);
329
- startOverBtn.addEventListener('click', async () => {
330
- stopWebcam();
331
- await resetFacialEmotions();
332
- // Reset UI
333
- progressBar.style.width = '0%';
334
- proceedBtn.disabled = true;
335
- setStatus('', 'white');
336
- analysisStatus.style.display = 'none';
337
- });
338
-
339
- // Automatically toggle the 'disabled-btn' class based on the 'disabled' attribute
340
- [proceedBtn, stopBtn, startOverBtn, startBtn].forEach(btn => {
341
- const observer = new MutationObserver(() => btn.classList.toggle('disabled-btn', btn.disabled));
342
- observer.observe(btn, { attributes: true, attributeFilter: ['disabled'] });
343
- });
344
-
345
- // Proceed button - save to localStorage before navigating
346
- proceedBtn.addEventListener('click', () => {
347
- // Ensure latest results are saved
348
- localStorage.setItem('facialEmotionResults', JSON.stringify(emotionResults));
349
- console.log("📦 Navigating to /voice with emotions:", emotionResults);
350
- window.location.href = "/voice";
351
- });
352
- </script>
353
  </body>
354
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
15
  body {
16
  font-family: 'Inter', sans-serif;
17
  }
18
+
19
  .btn-gradient {
20
  background-image: linear-gradient(to right, #6EE7B7, #3B82F6, #9333EA);
21
  transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
22
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
23
  }
24
+
25
  .btn-gradient:hover {
26
  transform: scale(1.05);
27
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
28
  }
29
+
30
  .video-container {
31
  position: relative;
32
  width: 100%;
 
38
  border: 3px solid #6EE7B7;
39
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
40
  }
41
+
42
  #webcam-video {
43
  display: none;
44
  width: 100%;
45
  height: 100%;
46
  object-fit: cover;
47
  }
48
+
49
  #placeholder-img {
50
  display: block;
51
  width: 100%;
52
  height: 100%;
53
  object-fit: cover;
54
  }
55
+
56
  #analysis-status {
57
  position: absolute;
58
  top: 1rem;
 
64
  font-weight: bold;
65
  display: none;
66
  }
67
+
68
  /* Styling for disabled buttons */
69
  .disabled-btn {
70
  background-image: none;
 
74
  transform: none;
75
  box-shadow: none;
76
  }
77
+
78
  .disabled-btn:hover {
79
  transform: none;
80
  box-shadow: none;
81
  }
82
+
83
  .loader {
84
  border: 4px solid #f3f3f3;
85
  border-top: 4px solid #3B82F6;
 
88
  height: 24px;
89
  animation: spin 1s linear infinite;
90
  }
91
+
92
  @keyframes spin {
93
+ 0% {
94
+ transform: rotate(0deg);
95
+ }
96
+
97
+ 100% {
98
+ transform: rotate(360deg);
99
+ }
100
  }
101
  </style>
102
  </head>
103
+
104
  <body class="bg-gray-900 text-gray-200">
105
 
106
  <div class="min-h-screen flex flex-col items-center justify-center p-4 md:p-8">
107
 
108
  <!-- Header -->
109
  <header class="flex items-center justify-center mb-8">
110
+ <svg class="h-10 w-10 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor"
111
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
112
  <path d="M12 21.5c-4.4 0-8-3.6-8-8 0-4.4 3.6-8 8-8s8 3.6 8 8c0 4.4-3.6 8-8 8z" />
113
  <path d="M12 10a2 2 0 100 4 2 2 0 000-4z" />
114
  <path d="M16 16l-1 1" />
 
116
  <path d="M16 8l1-1" />
117
  <path d="M8 8l-1-1" />
118
  </svg>
119
+ <h1
120
+ class="text-3xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-teal-400 via-blue-500 to-purple-600 ml-4">
121
  ShantiView
122
  </h1>
123
  </header>
 
128
  Facial Emotion Analysis
129
  </h2>
130
  <p id="instruction-message" class="text-base md:text-lg mb-8 text-gray-400 leading-relaxed">
131
+ Allow access to your webcam to begin the analysis. The "Proceed" button will be enabled once the
132
+ analysis is complete.
133
  </p>
134
 
135
  <!-- Video Feed Section -->
136
  <div class="video-container mx-auto mb-4" style="position:relative;">
137
  <!-- This video element is already in the DOM, so no need to append it via JS -->
138
  <video id="webcam-video" autoplay playsinline></video>
139
+ <img id="placeholder-img" src="https://placehold.co/900x506/1f2937/d1d5db?text=Webcam+Feed"
140
+ alt="Webcam Video Placeholder">
141
  <div id="analysis-status" class="text-lg"></div>
142
  <!-- Progress Bar -->
143
  <div class="w-full bg-gray-700 rounded-full h-2.5 mt-4">
144
+ <div id="progress-bar"
145
+ class="bg-teal-500 h-2.5 rounded-full transition-all duration-300 ease-in-out"
146
+ style="width: 0%;"></div>
147
  </div>
148
  </div>
149
 
150
  <!-- Control Buttons -->
151
  <div class="flex justify-center flex-wrap gap-4">
152
+ <button id="start-btn"
153
+ class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 bg-teal-500 hover:bg-teal-600">
154
  <span id="start-btn-text">Start</span>
155
  <div id="start-btn-loader" class="hidden loader"></div>
156
  </button>
157
+ <button id="stop-btn"
158
+ class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 bg-red-500 hover:bg-red-600 disabled-btn"
159
+ disabled>
160
  Stop
161
  </button>
162
+ <button id="proceed-btn"
163
+ class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 btn-gradient disabled-btn"
164
+ disabled>
165
  Proceed
166
  </button>
167
+ <button id="start-over-btn"
168
+ class="px-6 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 bg-gray-600 hover:bg-gray-700 disabled-btn"
169
+ disabled>
170
  Start Over
171
  </button>
172
  </div>
 
174
  </div>
175
 
176
  <script>
177
+ // Get references to all the important DOM elements
178
+ const webcamVideo = document.getElementById('webcam-video');
179
+ const placeholderImg = document.getElementById('placeholder-img');
180
+ const startBtn = document.getElementById('start-btn');
181
+ const stopBtn = document.getElementById('stop-btn');
182
+ const proceedBtn = document.getElementById('proceed-btn');
183
+ const startOverBtn = document.getElementById('start-over-btn');
184
+ const analysisStatus = document.getElementById('analysis-status');
185
+ const progressBar = document.getElementById('progress-bar');
186
+
187
+ // Create an off-screen canvas for capturing frames
188
+ const canvas = document.createElement('canvas');
189
+ canvas.width = 400;
190
+ canvas.height = 300;
191
+
192
+ // --- CLIENT-SIDE STATE ---
193
+ // This is the key fix: emotions are stored HERE, not on the server session.
194
+ let stream = null;
195
+ let intervalId = null;
196
+ let emotionResults = []; // Array to store captured emotion objects
197
+ let maxEmotions = 15;
198
+ let lastEmotion = null;
199
+ let isAnalyzing = false; // Prevent overlapping requests
200
+
201
+ // Function to update the status message on the screen
202
+ function setStatus(msg, color) {
203
+ analysisStatus.textContent = msg;
204
+ analysisStatus.style.display = 'block';
205
+ analysisStatus.style.color = color || 'white';
206
+ }
207
+
208
+ // Main function to start the webcam and begin analysis
209
+ async function startWebcam() {
210
+ try {
211
+ // Request access to the user's video camera
212
+ stream = await navigator.mediaDevices.getUserMedia({ video: true });
213
+
214
+ // Assign the stream to the video element and make it visible
215
+ webcamVideo.srcObject = stream;
216
+ webcamVideo.style.display = 'block';
217
+ placeholderImg.style.display = 'none';
218
+
219
+ // Reset client-side state
220
+ emotionResults = [];
221
+ lastEmotion = null;
222
+ isAnalyzing = false;
223
+ progressBar.style.width = '0%';
224
+ proceedBtn.disabled = true;
225
+ stopBtn.disabled = false;
226
+ startBtn.disabled = true;
227
+ startOverBtn.disabled = false;
228
+
229
+ setStatus('Capturing emotions...', '#38bdf8');
230
+
231
+ // Start capturing frames every 1.5 seconds (slightly slower to avoid overlap)
232
+ intervalId = setInterval(captureAndSendFrame, 1500);
233
+
234
+ } catch (e) {
235
+ console.error("Webcam access error:", e);
236
+ setStatus('Could not access webcam. Please allow permissions.', '#f87171');
237
+ startBtn.disabled = false;
238
+ }
239
+ }
240
 
241
+ // Function to stop the webcam stream
242
+ function stopWebcam() {
243
+ if (intervalId) {
244
+ clearInterval(intervalId);
245
+ intervalId = null;
246
+ }
247
+ if (stream) {
248
+ stream.getTracks().forEach(track => track.stop());
249
+ webcamVideo.srcObject = null;
250
+ stream = null;
251
+ }
252
 
253
+ // Update UI to show the placeholder again
254
+ webcamVideo.style.display = 'none';
255
+ placeholderImg.style.display = 'block';
256
 
257
+ // Update button states
258
+ setStatus('Webcam stopped.', '#fbbf24');
259
+ stopBtn.disabled = true;
260
  startBtn.disabled = false;
 
 
261
 
262
+ // Enable proceed if we captured at least one emotion
263
+ if (emotionResults.length > 0) {
264
+ proceedBtn.disabled = false;
265
+ }
 
 
 
 
 
 
266
  }
267
 
268
+ // Function to capture a frame from the video and send it for analysis
269
+ // Function to capture a frame from the video and send it for analysis
270
+ async function captureAndSendFrame() {
271
+ // Guard: prevent overlapping requests
272
+ if (isAnalyzing) {
273
+ console.log("Skipping frame - previous analysis still in progress");
274
+ return;
275
+ }
276
+ if (!webcamVideo.srcObject) return;
277
+ // Check if already done
278
+ if (emotionResults.length >= maxEmotions) return;
279
+
280
+ isAnalyzing = true;
281
+
282
+ const context = canvas.getContext('2d');
283
+ context.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
284
+ const dataUrl = canvas.toDataURL('image/jpeg');
285
+
286
+ try {
287
+ const resp = await fetch('/api/analyze-frame', {
288
+ method: 'POST',
289
+ headers: { 'Content-Type': 'application/json' },
290
+ body: JSON.stringify({ image: dataUrl }),
291
+ credentials: 'include'
292
+ });
293
 
294
+ if (!resp.ok) {
295
+ throw new Error(`API error: ${resp.statusText}`);
296
+ }
 
297
 
298
+ const data = await resp.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
+ // Debug: log the full response to see exactly what the server returns
301
+ console.log("Server response:", JSON.stringify(data));
302
 
303
+ lastEmotion = data.emotion || 'Unknown';
 
 
 
 
 
304
 
305
+ // Check if a confident emotion was detected
306
+ // Use explicit boolean check: data.detected === true
307
+ if (data.detected === true) {
308
+ emotionResults.push({
309
+ emotion: data.emotion,
310
+ score: data.score
311
+ });
312
 
313
+ // Keep only the last maxEmotions
314
+ if (emotionResults.length > maxEmotions) {
315
+ emotionResults = emotionResults.slice(-maxEmotions);
316
+ }
317
 
318
+ console.log(`✅ Emotion captured: ${data.emotion} (${data.score}). Total: ${emotionResults.length}/${maxEmotions}`);
319
+ } else {
320
+ console.log(`⏭️ Not counted: ${lastEmotion} (detected=${data.detected}). Count: ${emotionResults.length}/${maxEmotions}`);
321
+ }
322
 
323
+ // Update UI
324
+ setStatus(`Captured ${emotionResults.length}/${maxEmotions} emotions... Last: ${lastEmotion}`, '#a78bfa');
 
325
 
326
+ // Update progress bar
327
+ const percent = (emotionResults.length / maxEmotions) * 100;
328
+ progressBar.style.width = percent + '%';
 
 
329
 
330
+ // Check if analysis is complete
331
+ if (emotionResults.length >= maxEmotions) {
332
+ stopWebcam();
333
+ proceedBtn.disabled = false;
334
+ setStatus(`Analysis complete! Captured ${emotionResults.length} emotions. You may proceed.`, '#10b981');
335
+
336
+ // Store results in localStorage for the next page to use
337
+ localStorage.setItem('facialEmotionResults', JSON.stringify(emotionResults));
338
+ console.log("📦 Emotion results saved to localStorage:", emotionResults);
339
+ }
340
+ } catch (e) {
341
+ console.error("Error during frame analysis:", e);
342
+ setStatus('Error detecting emotion. Retrying...', '#f87171');
343
+ } finally {
344
+ isAnalyzing = false;
345
  }
 
 
 
 
 
346
  }
347
+
348
+ // Reset facial emotions analysis (client-side + server-side cleanup)
349
+ async function resetFacialEmotions() {
350
+ emotionResults = [];
351
+ localStorage.removeItem('facialEmotionResults');
352
+ try {
353
+ await fetch('/api/reset-emotions', { method: 'POST', credentials: 'include' });
354
+ } catch (e) {
355
+ console.log("Server reset call failed (non-critical):", e);
356
+ }
357
  }
358
+
359
+ // Event listeners for the control buttons
360
+ startBtn.addEventListener('click', async () => {
361
+ await resetFacialEmotions();
362
+ startWebcam();
363
+ });
364
+ stopBtn.addEventListener('click', stopWebcam);
365
+ startOverBtn.addEventListener('click', async () => {
366
+ stopWebcam();
367
+ await resetFacialEmotions();
368
+ // Reset UI
369
+ progressBar.style.width = '0%';
370
+ proceedBtn.disabled = true;
371
+ setStatus('', 'white');
372
+ analysisStatus.style.display = 'none';
373
+ });
374
+
375
+ // Automatically toggle the 'disabled-btn' class based on the 'disabled' attribute
376
+ [proceedBtn, stopBtn, startOverBtn, startBtn].forEach(btn => {
377
+ const observer = new MutationObserver(() => btn.classList.toggle('disabled-btn', btn.disabled));
378
+ observer.observe(btn, { attributes: true, attributeFilter: ['disabled'] });
379
+ });
380
+
381
+ // Proceed button - save to localStorage before navigating
382
+ proceedBtn.addEventListener('click', () => {
383
+ // Ensure latest results are saved
384
+ localStorage.setItem('facialEmotionResults', JSON.stringify(emotionResults));
385
+ console.log("📦 Navigating to /voice with emotions:", emotionResults);
386
+ window.location.href = "/voice";
387
+ });
388
+ </script>
 
389
  </body>
390
+
391
+ </html>
templates/results.html CHANGED
@@ -189,9 +189,10 @@
189
  </section>
190
 
191
  <div class="mt-8 text-center">
192
- <a href="{{ url_for('welcome') }}"
193
  class="inline-block btn-gradient px-8 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-transform"
194
- id="start-new-session-btn">
 
195
  Start New Session
196
  </a>
197
  </div>
@@ -263,18 +264,32 @@
263
  resultsIntro.textContent = `Here are some suggestions tailored just for you. ✨`;
264
  }
265
 
266
- // Function to fetch facial emotion data
267
  const fetchFacialEmotionData = async () => {
268
  try {
269
- // Call the correct endpoint to get the list of captured emotions.
270
- const response = await fetch('/api/get-emotions');
271
- if (!response.ok) {
272
- throw new Error(`HTTP error! status: ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
- const analysisData = await response.json();
275
- console.log("Facial Analysis Response:", analysisData);
276
- // Return the 'emotions' array.
277
- return analysisData.emotions;
278
  } catch (error) {
279
  console.error("Failed to fetch facial analysis data:", error);
280
  return null;
@@ -618,13 +633,14 @@
618
  const combinedAnalysisFeedback = document.getElementById('combined-analysis-feedback');
619
 
620
  try {
621
- // Fetch facial emotion data
622
  const facialEmotions = await fetchFacialEmotionData();
623
  if (!facialEmotions || facialEmotions.length === 0) {
624
  combinedAnalysisSummary.textContent = "No facial emotion data available for combined analysis.";
625
  combinedAnalysisFeedback.textContent = "";
626
  return;
627
  }
 
628
 
629
  // Fetch vocal emotion data
630
  const vocalEmotion = await fetchVoiceEmotion();
@@ -633,19 +649,20 @@
633
  combinedAnalysisFeedback.textContent = "";
634
  return;
635
  }
 
636
 
637
  // Perform the combined analysis API call
638
  const response = await fetch('/api/combined-analysis', {
639
- method: 'POST', // Change this from 'GET' to 'POST'
640
  headers: {
641
  'Content-Type': 'application/json',
642
  },
 
643
  body: JSON.stringify({
644
  facial_emotions_list: facialEmotions,
645
  vocal_emotion: vocalEmotion
646
  })
647
  });
648
-
649
  if (!response.ok) {
650
  throw new Error(`HTTP error! status: ${response.status}`);
651
  }
 
189
  </section>
190
 
191
  <div class="mt-8 text-center">
192
+ <a href="#"
193
  class="inline-block btn-gradient px-8 py-3 rounded-full text-lg font-bold text-white shadow-lg transition-transform"
194
+ id="start-new-session-btn"
195
+ onclick="localStorage.removeItem('facialEmotionResults'); window.location.href='/';">
196
  Start New Session
197
  </a>
198
  </div>
 
264
  resultsIntro.textContent = `Here are some suggestions tailored just for you. ✨`;
265
  }
266
 
267
+ // Function to fetch facial emotion data from localStorage (client-side state)
268
  const fetchFacialEmotionData = async () => {
269
  try {
270
+ // PRIMARY SOURCE: Read from localStorage (works on Hugging Face Spaces)
271
+ const storedData = localStorage.getItem('facialEmotionResults');
272
+ if (storedData) {
273
+ const emotions = JSON.parse(storedData);
274
+ if (emotions && emotions.length > 0) {
275
+ console.log("✅ Facial emotions loaded from localStorage:", emotions);
276
+ return emotions;
277
+ }
278
+ }
279
+
280
+ // FALLBACK: Try the server session endpoint (works locally)
281
+ console.log("⚠️ No facial data in localStorage, trying server session...");
282
+ const response = await fetch('/api/get-emotions', { credentials: 'include' });
283
+ if (response.ok) {
284
+ const analysisData = await response.json();
285
+ if (analysisData.emotions && analysisData.emotions.length > 0) {
286
+ console.log("✅ Facial emotions loaded from server session:", analysisData.emotions);
287
+ return analysisData.emotions;
288
+ }
289
  }
290
+
291
+ console.warn("⚠️ No facial emotion data found in either source.");
292
+ return null;
 
293
  } catch (error) {
294
  console.error("Failed to fetch facial analysis data:", error);
295
  return null;
 
633
  const combinedAnalysisFeedback = document.getElementById('combined-analysis-feedback');
634
 
635
  try {
636
+ // Fetch facial emotion data (now reads from localStorage first)
637
  const facialEmotions = await fetchFacialEmotionData();
638
  if (!facialEmotions || facialEmotions.length === 0) {
639
  combinedAnalysisSummary.textContent = "No facial emotion data available for combined analysis.";
640
  combinedAnalysisFeedback.textContent = "";
641
  return;
642
  }
643
+ console.log("📊 Combined analysis using facial emotions:", facialEmotions);
644
 
645
  // Fetch vocal emotion data
646
  const vocalEmotion = await fetchVoiceEmotion();
 
649
  combinedAnalysisFeedback.textContent = "";
650
  return;
651
  }
652
+ console.log("📊 Combined analysis using vocal emotion:", vocalEmotion);
653
 
654
  // Perform the combined analysis API call
655
  const response = await fetch('/api/combined-analysis', {
656
+ method: 'POST',
657
  headers: {
658
  'Content-Type': 'application/json',
659
  },
660
+ credentials: 'include',
661
  body: JSON.stringify({
662
  facial_emotions_list: facialEmotions,
663
  vocal_emotion: vocalEmotion
664
  })
665
  });
 
666
  if (!response.ok) {
667
  throw new Error(`HTTP error! status: ${response.status}`);
668
  }