Elliot89 commited on
Commit
53fd5bc
·
verified ·
1 Parent(s): 31d9b9d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -213
app.py CHANGED
@@ -1,213 +1,226 @@
1
- import os
2
- import datetime
3
- import logging
4
- import io
5
- import base64
6
- import uuid
7
-
8
- import cv2
9
- import pandas as pd
10
- import numpy as np
11
- import librosa
12
- import torch
13
- from transformers import Wav2Vec2ForSequenceClassification, Wav2Vec2FeatureExtractor
14
- from deepface import DeepFace
15
-
16
- from flask import Flask, request, jsonify, render_template
17
-
18
- # --- App & Logger Setup ---
19
- app = Flask(__name__)
20
- logging.basicConfig(
21
- level=logging.INFO,
22
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
- )
24
-
25
- # --- Constants & Directory Setup ---
26
- LOG_FILE = "wellbeing_logs.csv"
27
- CAPTURED_IMAGE_DIR = "captured_images"
28
- TEMP_AUDIO_DIR = "temp_audio"
29
-
30
- os.makedirs(CAPTURED_IMAGE_DIR, exist_ok=True)
31
- os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
32
-
33
- # --- Caching the Model ---
34
- voice_model = None
35
- voice_feature_extractor = None
36
-
37
- def load_voice_emotion_model():
38
- global voice_model, voice_feature_extractor
39
- if voice_model is None:
40
- logging.info("Loading voice emotion model for the first time...")
41
- model_name = "superb/wav2vec2-base-superb-er"
42
- voice_model = Wav2Vec2ForSequenceClassification.from_pretrained(model_name)
43
- voice_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_name)
44
- logging.info("Voice emotion model loaded.")
45
- return voice_model, voice_feature_extractor
46
-
47
- # --- Analysis Functions ---
48
- def analyze_voice_emotion(audio_file_path):
49
- try:
50
- model, feature_extractor = load_voice_emotion_model()
51
- y, sr = librosa.load(audio_file_path, sr=16000, mono=True)
52
- if y.shape[0] == 0:
53
- logging.warning(f"Audio file {audio_file_path} was empty.")
54
- return "Error: Invalid or empty audio"
55
- inputs = feature_extractor(y, sampling_rate=sr, return_tensors="pt", padding=True)
56
- with torch.no_grad():
57
- logits = model(**inputs).logits
58
- predicted_id = torch.argmax(logits, dim=-1).item()
59
- return model.config.id2label[predicted_id]
60
- except Exception as e:
61
- logging.exception(f"Voice emotion analysis failed for file {audio_file_path}: {e}")
62
- return "Error: Voice analysis failed"
63
-
64
- def analyze_emotion_from_data(image_bytes, detector_backend="retinaface"):
65
- try:
66
- nparr = np.frombuffer(image_bytes, np.uint8)
67
- img_np = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
68
- if img_np is None:
69
- return "Error: Could not decode image"
70
-
71
- # Use a fallback detector if the selected one fails
72
- try:
73
- result = DeepFace.analyze(
74
- img_path=img_np, actions=['emotion'],
75
- detector_backend=detector_backend, enforce_detection=False
76
- )
77
- except Exception as detector_error:
78
- logging.warning(f"Detector '{detector_backend}' failed: {detector_error}. Falling back to 'opencv'.")
79
- result = DeepFace.analyze(
80
- img_path=img_np, actions=['emotion'],
81
- detector_backend='opencv', enforce_detection=False
82
- )
83
-
84
- if isinstance(result, list) and len(result) > 0:
85
- return result[0].get("dominant_emotion", "No face detected")
86
- else:
87
- return "No face detected"
88
- except Exception as e:
89
- logging.exception(f"Face emotion analysis failed with backend {detector_backend}: {e}")
90
- return "Error: Face analysis failed"
91
-
92
- def assess_stress_enhanced(face_emotion, sleep_hours, activity_level, voice_emotion):
93
- activity_map = {"Very Low": 3, "Low": 2, "Moderate": 1, "High": 0}
94
- emotion_map = { "angry": 2, "disgust": 2, "fear": 2, "sad": 2, "neutral": 1, "surprise": 1, "happy": 0 }
95
- face_emotion_score = emotion_map.get(str(face_emotion).lower(), 1)
96
- voice_emotion_score = emotion_map.get(str(voice_emotion).lower(), 1)
97
- emotion_score = round((face_emotion_score + voice_emotion_score) / 2) if voice_emotion != "N/A" else face_emotion_score
98
- activity_score = activity_map.get(str(activity_level), 1)
99
- try:
100
- sleep_hours = float(sleep_hours)
101
- sleep_score = 0 if sleep_hours >= 7 else (1 if sleep_hours >= 5 else 2)
102
- except (ValueError, TypeError):
103
- sleep_score, sleep_hours = 2, 0
104
- stress_score = emotion_score + activity_score + sleep_score
105
- feedback = f"**Your potential stress score is {stress_score} (lower is better).**\n\n**Breakdown:**\n"
106
- feedback += f"- Face Emotion: {face_emotion} (score: {face_emotion_score})\n"
107
- feedback += f"- Voice Emotion: {voice_emotion} (score: {voice_emotion_score})\n"
108
- feedback += f"- Sleep: {sleep_hours} hours (score: {sleep_score})\n"
109
- feedback += f"- Activity: {activity_level} (score: {activity_score})\n"
110
- if stress_score <= 2:
111
- feedback += "\nGreat job! You seem to be in a good space."
112
- elif stress_score <= 4:
113
- feedback += "\nYou're doing okay, but remember to be mindful of your rest and mood."
114
- else:
115
- feedback += "\nConsider taking some time for self-care. Improving sleep or gentle activity might help."
116
- return feedback, stress_score
117
-
118
- # --- Flask Routes ---
119
- @app.route('/')
120
- def index():
121
- return render_template('index.html')
122
-
123
- @app.route('/analyze_face', methods=['POST'])
124
- def analyze_face_endpoint():
125
- data = request.json
126
- detector = data.get('detector', 'retinaface')
127
- image_data = base64.b64decode(data['image'].split(',')[1])
128
- emotion = analyze_emotion_from_data(image_data, detector_backend=detector)
129
- image_path = "N/A"
130
- if not emotion.startswith("Error:") and not emotion == "No face detected":
131
- filename = f"face_{uuid.uuid4()}.jpg"
132
- image_path = os.path.join(CAPTURED_IMAGE_DIR, filename)
133
- with open(image_path, "wb") as f:
134
- f.write(image_data)
135
- return jsonify({'emotion': emotion, 'image_path': image_path})
136
-
137
- @app.route('/analyze_voice', methods=['POST'])
138
- def analyze_voice_endpoint():
139
- audio_file = request.files.get('audio')
140
- if not audio_file:
141
- return jsonify({'error': 'No audio file provided'}), 400
142
- temp_filename = f"{uuid.uuid4()}.webm"
143
- temp_filepath = os.path.join(TEMP_AUDIO_DIR, temp_filename)
144
- try:
145
- audio_file.save(temp_filepath)
146
- emotion = analyze_voice_emotion(temp_filepath)
147
- finally:
148
- if os.path.exists(temp_filepath):
149
- os.remove(temp_filepath)
150
- return jsonify({'voice_emotion': emotion})
151
-
152
- @app.route('/log_checkin', methods=['POST'])
153
- def log_checkin_endpoint():
154
- data = request.json
155
- feedback, stress_score = assess_stress_enhanced(
156
- data['emotion'], data['sleep_hours'], data['activity_level'], data['voice_emotion']
157
- )
158
- # *** FIX: Format timestamp as a consistent string BEFORE saving ***
159
- new_log_entry = {
160
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
161
- "face_emotion": data['emotion'],
162
- "voice_emotion": data.get('voice_emotion', 'N/A'),
163
- "sleep_hours": data['sleep_hours'],
164
- "activity_level": data['activity_level'],
165
- "stress_score": stress_score,
166
- "detector_backend": data.get('detector', 'retinaface'),
167
- "image_path": data.get('image_path', 'N/A')
168
- }
169
- try:
170
- header = not os.path.exists(LOG_FILE)
171
- df_new = pd.DataFrame([new_log_entry])
172
- df_new.to_csv(LOG_FILE, mode='a', header=header, index=False)
173
- return jsonify({'feedback': feedback, 'stress_score': stress_score, 'status': 'success'})
174
- except Exception as e:
175
- logging.exception(f"Could not save log: {e}")
176
- return jsonify({'error': f'Could not save log: {e}'}), 500
177
-
178
- @app.route('/get_logs', methods=['GET'])
179
- def get_logs_endpoint():
180
- if not os.path.exists(LOG_FILE):
181
- return jsonify({'data': [], 'columns': []})
182
- try:
183
- df = pd.read_csv(LOG_FILE)
184
- # *** FIX: No need to parse/reformat timestamps. They are already correct strings. ***
185
- return jsonify({
186
- 'data': df.to_dict(orient='records'),
187
- 'columns': df.columns.tolist()
188
- })
189
- except pd.errors.EmptyDataError:
190
- return jsonify({'data': [], 'columns': []})
191
- except Exception as e:
192
- logging.exception(f"Could not read logs: {e}")
193
- return jsonify({'error': 'Could not read logs'}), 500
194
-
195
- @app.route('/clear_logs', methods=['POST'])
196
- def clear_logs_endpoint():
197
- try:
198
- if os.path.exists(LOG_FILE):
199
- os.remove(LOG_FILE)
200
- for directory in [CAPTURED_IMAGE_DIR, TEMP_AUDIO_DIR]:
201
- if os.path.exists(directory):
202
- for f in os.listdir(directory):
203
- os.remove(os.path.join(directory, f))
204
- return jsonify({'status': 'success', 'message': 'All logs and images cleared.'})
205
- except Exception as e:
206
- logging.exception(f"Error clearing logs: {e}")
207
- return jsonify({'status': 'error', 'message': str(e)}), 500
208
-
209
- if __name__ == '__main__':
210
- load_voice_emotion_model()
211
- # Hugging Face requires port 7860
212
- port = int(os.environ.get('PORT', 7860))
213
- app.run(debug=False, host='0.0.0.0', port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import datetime
3
+ import logging
4
+ import io
5
+ import base64
6
+ import uuid
7
+
8
+ import cv2
9
+ import pandas as pd
10
+ import numpy as np
11
+ import librosa
12
+ import torch
13
+ from transformers import Wav2Vec2ForSequenceClassification, Wav2Vec2FeatureExtractor
14
+ from deepface import DeepFace
15
+
16
+ from flask import Flask, request, jsonify, render_template
17
+ import sys
18
+
19
+ # Force port 7860 for Hugging Face Spaces
20
+ os.environ['PORT'] = '7860'
21
+ # --- App & Logger Setup ---
22
+ app = Flask(__name__)
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
26
+ )
27
+
28
+ # --- Constants & Directory Setup ---
29
+ LOG_FILE = "wellbeing_logs.csv"
30
+ CAPTURED_IMAGE_DIR = "captured_images"
31
+ TEMP_AUDIO_DIR = "temp_audio"
32
+
33
+ os.makedirs(CAPTURED_IMAGE_DIR, exist_ok=True)
34
+ os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
35
+
36
+ # --- Caching the Model ---
37
+ voice_model = None
38
+ voice_feature_extractor = None
39
+
40
+ def load_voice_emotion_model():
41
+ global voice_model, voice_feature_extractor
42
+ if voice_model is None:
43
+ logging.info("Loading voice emotion model for the first time...")
44
+ model_name = "superb/wav2vec2-base-superb-er"
45
+ voice_model = Wav2Vec2ForSequenceClassification.from_pretrained(model_name)
46
+ voice_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_name)
47
+ logging.info("Voice emotion model loaded.")
48
+ return voice_model, voice_feature_extractor
49
+
50
+ # --- Analysis Functions ---
51
+ def analyze_voice_emotion(audio_file_path):
52
+ try:
53
+ model, feature_extractor = load_voice_emotion_model()
54
+ y, sr = librosa.load(audio_file_path, sr=16000, mono=True)
55
+ if y.shape[0] == 0:
56
+ logging.warning(f"Audio file {audio_file_path} was empty.")
57
+ return "Error: Invalid or empty audio"
58
+ inputs = feature_extractor(y, sampling_rate=sr, return_tensors="pt", padding=True)
59
+ with torch.no_grad():
60
+ logits = model(**inputs).logits
61
+ predicted_id = torch.argmax(logits, dim=-1).item()
62
+ return model.config.id2label[predicted_id]
63
+ except Exception as e:
64
+ logging.exception(f"Voice emotion analysis failed for file {audio_file_path}: {e}")
65
+ return "Error: Voice analysis failed"
66
+
67
+ def analyze_emotion_from_data(image_bytes, detector_backend="retinaface"):
68
+ try:
69
+ nparr = np.frombuffer(image_bytes, np.uint8)
70
+ img_np = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
71
+ if img_np is None:
72
+ return "Error: Could not decode image"
73
+
74
+ # Use a fallback detector if the selected one fails
75
+ try:
76
+ result = DeepFace.analyze(
77
+ img_path=img_np, actions=['emotion'],
78
+ detector_backend=detector_backend, enforce_detection=False
79
+ )
80
+ except Exception as detector_error:
81
+ logging.warning(f"Detector '{detector_backend}' failed: {detector_error}. Falling back to 'opencv'.")
82
+ result = DeepFace.analyze(
83
+ img_path=img_np, actions=['emotion'],
84
+ detector_backend='opencv', enforce_detection=False
85
+ )
86
+
87
+ if isinstance(result, list) and len(result) > 0:
88
+ return result[0].get("dominant_emotion", "No face detected")
89
+ else:
90
+ return "No face detected"
91
+ except Exception as e:
92
+ logging.exception(f"Face emotion analysis failed with backend {detector_backend}: {e}")
93
+ return "Error: Face analysis failed"
94
+
95
+ def assess_stress_enhanced(face_emotion, sleep_hours, activity_level, voice_emotion):
96
+ activity_map = {"Very Low": 3, "Low": 2, "Moderate": 1, "High": 0}
97
+ emotion_map = { "angry": 2, "disgust": 2, "fear": 2, "sad": 2, "neutral": 1, "surprise": 1, "happy": 0 }
98
+ face_emotion_score = emotion_map.get(str(face_emotion).lower(), 1)
99
+ voice_emotion_score = emotion_map.get(str(voice_emotion).lower(), 1)
100
+ emotion_score = round((face_emotion_score + voice_emotion_score) / 2) if voice_emotion != "N/A" else face_emotion_score
101
+ activity_score = activity_map.get(str(activity_level), 1)
102
+ try:
103
+ sleep_hours = float(sleep_hours)
104
+ sleep_score = 0 if sleep_hours >= 7 else (1 if sleep_hours >= 5 else 2)
105
+ except (ValueError, TypeError):
106
+ sleep_score, sleep_hours = 2, 0
107
+ stress_score = emotion_score + activity_score + sleep_score
108
+ feedback = f"**Your potential stress score is {stress_score} (lower is better).**\n\n**Breakdown:**\n"
109
+ feedback += f"- Face Emotion: {face_emotion} (score: {face_emotion_score})\n"
110
+ feedback += f"- Voice Emotion: {voice_emotion} (score: {voice_emotion_score})\n"
111
+ feedback += f"- Sleep: {sleep_hours} hours (score: {sleep_score})\n"
112
+ feedback += f"- Activity: {activity_level} (score: {activity_score})\n"
113
+ if stress_score <= 2:
114
+ feedback += "\nGreat job! You seem to be in a good space."
115
+ elif stress_score <= 4:
116
+ feedback += "\nYou're doing okay, but remember to be mindful of your rest and mood."
117
+ else:
118
+ feedback += "\nConsider taking some time for self-care. Improving sleep or gentle activity might help."
119
+ return feedback, stress_score
120
+
121
+ # --- Flask Routes ---
122
+ @app.route('/')
123
+ def index():
124
+ return render_template('index.html')
125
+
126
+ @app.route('/analyze_face', methods=['POST'])
127
+ def analyze_face_endpoint():
128
+ data = request.json
129
+ detector = data.get('detector', 'retinaface')
130
+ image_data = base64.b64decode(data['image'].split(',')[1])
131
+ emotion = analyze_emotion_from_data(image_data, detector_backend=detector)
132
+ image_path = "N/A"
133
+ if not emotion.startswith("Error:") and not emotion == "No face detected":
134
+ filename = f"face_{uuid.uuid4()}.jpg"
135
+ image_path = os.path.join(CAPTURED_IMAGE_DIR, filename)
136
+ with open(image_path, "wb") as f:
137
+ f.write(image_data)
138
+ return jsonify({'emotion': emotion, 'image_path': image_path})
139
+
140
+ @app.route('/analyze_voice', methods=['POST'])
141
+ def analyze_voice_endpoint():
142
+ audio_file = request.files.get('audio')
143
+ if not audio_file:
144
+ return jsonify({'error': 'No audio file provided'}), 400
145
+ temp_filename = f"{uuid.uuid4()}.webm"
146
+ temp_filepath = os.path.join(TEMP_AUDIO_DIR, temp_filename)
147
+ try:
148
+ audio_file.save(temp_filepath)
149
+ emotion = analyze_voice_emotion(temp_filepath)
150
+ finally:
151
+ if os.path.exists(temp_filepath):
152
+ os.remove(temp_filepath)
153
+ return jsonify({'voice_emotion': emotion})
154
+
155
+ @app.route('/log_checkin', methods=['POST'])
156
+ def log_checkin_endpoint():
157
+ data = request.json
158
+ feedback, stress_score = assess_stress_enhanced(
159
+ data['emotion'], data['sleep_hours'], data['activity_level'], data['voice_emotion']
160
+ )
161
+ # *** FIX: Format timestamp as a consistent string BEFORE saving ***
162
+ new_log_entry = {
163
+ "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
164
+ "face_emotion": data['emotion'],
165
+ "voice_emotion": data.get('voice_emotion', 'N/A'),
166
+ "sleep_hours": data['sleep_hours'],
167
+ "activity_level": data['activity_level'],
168
+ "stress_score": stress_score,
169
+ "detector_backend": data.get('detector', 'retinaface'),
170
+ "image_path": data.get('image_path', 'N/A')
171
+ }
172
+ try:
173
+ header = not os.path.exists(LOG_FILE)
174
+ df_new = pd.DataFrame([new_log_entry])
175
+ df_new.to_csv(LOG_FILE, mode='a', header=header, index=False)
176
+ return jsonify({'feedback': feedback, 'stress_score': stress_score, 'status': 'success'})
177
+ except Exception as e:
178
+ logging.exception(f"Could not save log: {e}")
179
+ return jsonify({'error': f'Could not save log: {e}'}), 500
180
+
181
+ @app.route('/get_logs', methods=['GET'])
182
+ def get_logs_endpoint():
183
+ if not os.path.exists(LOG_FILE):
184
+ return jsonify({'data': [], 'columns': []})
185
+ try:
186
+ df = pd.read_csv(LOG_FILE)
187
+ # *** FIX: No need to parse/reformat timestamps. They are already correct strings. ***
188
+ return jsonify({
189
+ 'data': df.to_dict(orient='records'),
190
+ 'columns': df.columns.tolist()
191
+ })
192
+ except pd.errors.EmptyDataError:
193
+ return jsonify({'data': [], 'columns': []})
194
+ except Exception as e:
195
+ logging.exception(f"Could not read logs: {e}")
196
+ return jsonify({'error': 'Could not read logs'}), 500
197
+
198
+ @app.route('/clear_logs', methods=['POST'])
199
+ def clear_logs_endpoint():
200
+ try:
201
+ if os.path.exists(LOG_FILE):
202
+ os.remove(LOG_FILE)
203
+ for directory in [CAPTURED_IMAGE_DIR, TEMP_AUDIO_DIR]:
204
+ if os.path.exists(directory):
205
+ for f in os.listdir(directory):
206
+ os.remove(os.path.join(directory, f))
207
+ return jsonify({'status': 'success', 'message': 'All logs and images cleared.'})
208
+ except Exception as e:
209
+ logging.exception(f"Error clearing logs: {e}")
210
+ return jsonify({'status': 'error', 'message': str(e)}), 500
211
+
212
+ if __name__ == '__main__':
213
+ # Load models at startup
214
+ load_voice_emotion_model()
215
+
216
+ # Hugging Face Spaces requires port 7860
217
+ port = int(os.environ.get('PORT', 7860))
218
+
219
+ # Run with production settings
220
+ app.run(
221
+ host='0.0.0.0',
222
+ port=port,
223
+ debug=False,
224
+ threaded=True
225
+ )
226
+