Testys commited on
Commit
447da48
Β·
verified Β·
1 Parent(s): 653fae1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -139
app.py CHANGED
@@ -1,11 +1,4 @@
1
- # app_gradio.py
2
- # ──────────────────────────────────────────────────────────
3
- # Webcam β†’ geometric detector β†’ static WAV alert (with cooldown)
4
- # Live console logs of per-frame latency + status.
5
- #
6
- # EDITED: This version uses a more robust method for audio playback
7
- # in Gradio by dynamically creating the Audio component.
8
- # ──────────────────────────────────────────────────────────
9
  import time
10
  import os
11
  import yaml
@@ -14,9 +7,9 @@ import numpy as np
14
  import gradio as gr
15
  import soundfile as sf
16
  from dotenv import load_dotenv
 
17
 
18
- # This is a mock factory and detector for demonstration.
19
- # Replace with your actual import.
20
  from src.detection.factory import get_detector
21
 
22
  # ───────────────────────────── logging
@@ -28,86 +21,72 @@ logging.basicConfig(
28
 
29
  # ───────────────────────────── config / detector
30
  load_dotenv()
31
- with open("config.yaml") as f:
32
- CFG = yaml.safe_load(f)
 
 
 
 
 
 
33
 
34
  detector = get_detector(CFG)
35
 
36
- # ───────────────────────────── Alert Manager Class <--- CHANGE
37
- # Encapsulating the alert logic makes the code much cleaner.
38
- # It handles its own state (last alert time) internally.
39
  class AlertManager:
40
  def __init__(self, config):
41
  self.cooldown_seconds = config.get("alert_cooldown_seconds", 5)
42
  self.last_alert_time = 0
43
- self.alert_data = None
44
- self.sample_rate = None
45
- # --- NEW: State variable to track if an alert is active ---
46
  self.is_alert_active = False
47
  self._load_sound(config.get("alert_sound_path"))
48
 
49
  def _load_sound(self, wav_path):
50
- if not wav_path:
51
- logging.warning("No 'alert_sound_path' found in config.")
 
 
52
  return
53
  try:
54
- # Load as int16 to avoid the Gradio conversion warning
55
  data, sr = sf.read(wav_path, dtype="int16")
56
  self.alert_data = data
57
  self.sample_rate = sr
58
- logging.info(f"Loaded alert sound: {wav_path} ({len(self.alert_data)/self.sample_rate:.2f}s)")
59
  except Exception as e:
60
  logging.error(f"Failed to load alert sound: {e}")
61
  self.alert_data = None
62
 
63
  def trigger_alert(self, level, lighting):
64
- """
65
- Checks conditions and returns audio payload if a new alert should fire.
66
- This is now stateful.
67
- """
68
- # --- NEW LOGIC: Part 1 ---
69
- # If an alert is currently active, we do nothing until the user is 'Awake'.
70
  if self.is_alert_active:
71
  if level == "Awake":
72
  logging.info("βœ… Alert state reset. User is Awake. Re-arming system.")
73
  self.is_alert_active = False
74
- return None # Important: Return None to prevent any sound
75
 
76
- # --- ORIGINAL LOGIC (with a small change) ---
77
- # If no alert is active, check for conditions to fire a new one.
78
  is_drowsy = level != "Awake"
79
  is_good_light = lighting != "Low"
80
- # The time-based cooldown is still useful to prevent flickering alerts.
81
  is_ready = (time.monotonic() - self.last_alert_time) > self.cooldown_seconds
82
 
83
  if self.alert_data is not None and is_drowsy and is_good_light and is_ready:
84
  self.last_alert_time = time.monotonic()
85
- # --- NEW LOGIC: Part 2 ---
86
- # Set the alert to active so it doesn't fire again immediately.
87
  self.is_alert_active = True
88
- logging.info("πŸ”Š Drowsiness detected! Firing alert and setting state to active.")
89
  return (self.sample_rate, self.alert_data.copy())
90
-
91
  return None
92
- # Initialize the alert manager
93
- alert_manager = AlertManager(CFG["alerting"])
94
 
95
- # ───────────────────────────── frame processing <--- MAJOR CHANGE
 
 
96
  def process_live_frame(frame):
97
  if frame is None:
98
- return (
99
- np.zeros((480, 640, 3), dtype=np.uint8),
100
- "Status: Inactive",
101
- None # No audio output
102
- )
103
 
104
  t0 = time.perf_counter()
105
-
106
  try:
107
- processed, indic = detector.process_frame(frame)
 
108
  except Exception as e:
109
  logging.error(f"Error processing frame: {e}")
110
- processed = np.zeros_like(frame)
111
  indic = {"drowsiness_level": "Error", "lighting": "Unknown", "details": {"Score": 0.0}}
112
 
113
  level = indic.get("drowsiness_level", "Awake")
@@ -115,23 +94,16 @@ def process_live_frame(frame):
115
  score = indic.get("details", {}).get("Score", 0.0)
116
 
117
  dt_ms = (time.perf_counter() - t0) * 1000.0
118
- logging.info(f"{dt_ms:6.1f} ms β”‚ {lighting:<4} β”‚ {level:<14} β”‚ score={score:.2f}")
119
 
120
- status_txt = (
121
- f"Lighting: {lighting}\n"
122
- + ("Detection paused – low light." if lighting == "Low"
123
- else f"Status: {level}\nScore: {score:.2f}")
124
- )
125
 
126
  audio_payload = alert_manager.trigger_alert(level, lighting)
 
127
 
128
- if audio_payload:
129
- return processed, status_txt, gr.Audio(value=audio_payload, autoplay=True)
130
- else:
131
- return processed, status_txt, None
132
 
133
-
134
- # ───────────────────────────── NEW: Frame Processing for Tab 2 (Analysis-Only)
135
  def process_for_stats_only(frame):
136
  """
137
  Processes a frame but does not return any video/image output.
@@ -142,8 +114,8 @@ def process_for_stats_only(frame):
142
 
143
  t0 = time.perf_counter()
144
  try:
145
- # We still call the same detector, but we will ignore the processed frame it returns.
146
- _, indic = detector.analyse_frame(frame)
147
  except Exception as e:
148
  logging.error(f"Error processing frame: {e}")
149
  indic = {"drowsiness_level": "Error", "lighting": "Unknown", "details": {"Score": 0.0}}
@@ -166,86 +138,72 @@ def process_for_stats_only(frame):
166
 
167
  return status_txt, audio_out
168
 
169
-
170
- # ───────────────────────────── UI Definition
171
  def create_readme_tab():
172
  """Creates the content for the 'About' tab."""
173
- with gr.Blocks(title="Drive Paddy - About Page") as readme_tab:
174
- gr.Markdown(
175
- """
176
- <div align="center">
177
- <img src="https://em-content.zobj.net/source/samsung/380/automobile_1f697.png" alt="Car Emoji" width="100"/>
178
- <h1>Drive Paddy</h1>
179
- <p><strong>Your Drowsiness Detection Assistant</strong></p>
180
- </div>
181
-
182
- ---
183
-
184
- ## 🌟 Features
185
- - **Real-Time Webcam Streaming**: Directly processes your live camera feed for immediate feedback.
186
- - **Efficient Geometric Analysis**: Uses `MediaPipe` for high-performance facial landmark detection.
187
- - **Multi-Signal Analysis**: Detects eye closure (EAR), yawns (MAR), and head-nodding.
188
- - **Stateful Alert System**: Plays a clear audio alert for new drowsiness events and intelligently re-arms itself, preventing alert fatigue.
189
- - **Low-Light Warning**: Automatically detects and warns about poor lighting conditions.
190
- - **Configurable**: Key detection thresholds and settings can be tuned via `config.yaml`.
191
-
192
- ---
193
-
194
- ## πŸ› οΈ How It Works
195
- 1. **Video Streaming**: The `gradio.Image` component captures the camera feed.
196
- 2. **Frame Processing**: Each frame is sent to the `GeometricProcessor`.
197
- 3. **Stateful Alerting**: The `AlertManager` class uses internal state to decide if a *new* alert should be triggered.
198
- 4. **Dynamic Updates**: The processed video, status text, and audio alerts are sent back to the frontend for a seamless real-time experience.
199
-
200
- ---
201
-
202
- ## πŸ’‘ Understanding the Live Status
203
- The status panel provides real-time feedback on the following parameters:
204
-
205
- - **`Lighting`**: Indicates the ambient light conditions.
206
- - `Good`: Sufficient light for reliable detection.
207
- - `Low`: Insufficient light. Detection is paused as the results would be unreliable.
208
-
209
- - **`Status`**: The overall assessed level of driver alertness.
210
- - `Awake`: The driver appears alert.
211
- - `Slightly Drowsy`: Early signs of fatigue have been detected.
212
- - `Very Drowsy`: Strong indicators of drowsiness are present. An alert is triggered.
213
-
214
- - **`Score`**: A numerical value representing the accumulated evidence of drowsiness based on the weighted indicators (eye closure, yawning, head pose). A higher score corresponds to a greater level of detected drowsiness.
215
- """
216
- )
217
- return readme_tab
218
 
219
-
220
- # ───────────────────────────── UI <--- CHANGE
221
  def create_detection_tab():
222
- with gr.Blocks(title="Drive Paddy – πŸ“Ή Live Drowsiness Detection Tab") as detection_tab:
223
- gr.Markdown("## πŸ“Ή Live Drowsiness Detection")
224
- gr.Markdown("Press 'START' to activate your camera and begin monitoring. The console will show real-time logs.")
225
-
226
- with gr.Row():
227
- with gr.Column(scale=2):
228
- cam = gr.Image(sources=["webcam"], streaming=True, label="Live Camera Feed")
229
- with gr.Column(scale=1):
230
- out_img = gr.Image(label="Processed Feed")
231
- out_text = gr.Textbox(label="Live Status", lines=3, interactive=False)
232
-
233
- # This audio component now acts as a placeholder.
234
- # We make it invisible because we don't need to show the player controls.
235
- # The backend will dynamically send a new, playable component to it.
236
- out_audio = gr.Audio(
237
- label="Alert",
238
- autoplay=True,
239
- visible=False, # Hiding the component for a cleaner UX
240
- )
241
 
242
- # The gr.State for managing the timestamp is no longer needed, simplifying the stream call.
243
- cam.stream(
244
- fn=process_live_frame,
245
- inputs=[cam],
246
- outputs=[out_img, out_text, out_audio] # The output now targets the placeholder
247
- )
248
-
249
  def create_analysis_only_tab():
250
  """Creates the content for the Analysis-Only Mode tab."""
251
  gr.Markdown("## ⚑ Analysis-Only Mode")
@@ -265,7 +223,9 @@ def create_analysis_only_tab():
265
  outputs=[out_text_analysis, out_audio_analysis]
266
  )
267
 
268
- with gr.Blocks(title="πŸš— Drive Paddy – Drowsiness Detection", theme=gr.themes.Soft()) as app:
 
 
269
  gr.Markdown("# πŸš— **Drive Paddy**")
270
  with gr.Tabs():
271
  with gr.TabItem("Live Detection"):
@@ -276,5 +236,5 @@ with gr.Blocks(title="πŸš— Drive Paddy – Drowsiness Detection", theme=gr.theme
276
  create_readme_tab()
277
 
278
  if __name__ == "__main__":
279
- logging.info("Launching Gradio app…")
280
- app.launch(debug=True)
 
1
+ # app.py
 
 
 
 
 
 
 
2
  import time
3
  import os
4
  import yaml
 
7
  import gradio as gr
8
  import soundfile as sf
9
  from dotenv import load_dotenv
10
+ import cv2 # Retained for detector compatibility
11
 
12
+ # Assuming the factory and processor are in the src directory
 
13
  from src.detection.factory import get_detector
14
 
15
  # ───────────────────────────── logging
 
21
 
22
  # ───────────────────────────── config / detector
23
  load_dotenv()
24
+ try:
25
+ with open("config.yaml") as f:
26
+ CFG = yaml.safe_load(f)
27
+ except FileNotFoundError:
28
+ logging.error("FATAL: config.yaml not found. Please ensure the file exists.")
29
+ # Create a dummy CFG to prevent crashing the app on load
30
+ CFG = {"alerting": {}, "geometric_settings": {}}
31
+
32
 
33
  detector = get_detector(CFG)
34
 
35
+ # ───────────────────────────── Alert Manager Class (Unchanged)
 
 
36
  class AlertManager:
37
  def __init__(self, config):
38
  self.cooldown_seconds = config.get("alert_cooldown_seconds", 5)
39
  self.last_alert_time = 0
 
 
 
40
  self.is_alert_active = False
41
  self._load_sound(config.get("alert_sound_path"))
42
 
43
  def _load_sound(self, wav_path):
44
+ if not wav_path or not os.path.exists(wav_path):
45
+ logging.warning(f"Alert sound not found at '{wav_path}'. Alerts will be silent.")
46
+ self.alert_data = None
47
+ self.sample_rate = None
48
  return
49
  try:
 
50
  data, sr = sf.read(wav_path, dtype="int16")
51
  self.alert_data = data
52
  self.sample_rate = sr
53
+ logging.info(f"Loaded alert sound: {wav_path}")
54
  except Exception as e:
55
  logging.error(f"Failed to load alert sound: {e}")
56
  self.alert_data = None
57
 
58
  def trigger_alert(self, level, lighting):
 
 
 
 
 
 
59
  if self.is_alert_active:
60
  if level == "Awake":
61
  logging.info("βœ… Alert state reset. User is Awake. Re-arming system.")
62
  self.is_alert_active = False
63
+ return None
64
 
 
 
65
  is_drowsy = level != "Awake"
66
  is_good_light = lighting != "Low"
 
67
  is_ready = (time.monotonic() - self.last_alert_time) > self.cooldown_seconds
68
 
69
  if self.alert_data is not None and is_drowsy and is_good_light and is_ready:
70
  self.last_alert_time = time.monotonic()
 
 
71
  self.is_alert_active = True
72
+ logging.info("πŸ”Š Drowsiness detected! Firing alert.")
73
  return (self.sample_rate, self.alert_data.copy())
 
74
  return None
 
 
75
 
76
+ alert_manager = AlertManager(CFG.get("alerting", {}))
77
+
78
+ # ───────────────────────────── Frame Processing for Tab 1 (Image Stream) - UPDATED
79
  def process_live_frame(frame):
80
  if frame is None:
81
+ return (np.zeros((480, 640, 3), dtype=np.uint8), "Status: Inactive", None)
 
 
 
 
82
 
83
  t0 = time.perf_counter()
 
84
  try:
85
+ # --- FIX: Call with draw_visuals=True ---
86
+ processed, indic = detector.process_frame(frame, draw_visuals=True)
87
  except Exception as e:
88
  logging.error(f"Error processing frame: {e}")
89
+ processed = np.zeros_like(frame) if frame is not None else np.zeros((480, 640, 3), dtype=np.uint8)
90
  indic = {"drowsiness_level": "Error", "lighting": "Unknown", "details": {"Score": 0.0}}
91
 
92
  level = indic.get("drowsiness_level", "Awake")
 
94
  score = indic.get("details", {}).get("Score", 0.0)
95
 
96
  dt_ms = (time.perf_counter() - t0) * 1000.0
97
+ logging.info(f"IMAGE STREAM β”‚ {dt_ms:6.1f} ms β”‚ {lighting:<4} β”‚ {level:<14} β”‚ score={score:.2f}")
98
 
99
+ status_txt = f"Lighting: {lighting}\n" + \
100
+ ("Detection paused – low light." if lighting == "Low" else f"Status: {level}\nScore: {score:.2f}")
 
 
 
101
 
102
  audio_payload = alert_manager.trigger_alert(level, lighting)
103
+ return processed, status_txt, gr.Audio(value=audio_payload, autoplay=True) if audio_payload else None
104
 
 
 
 
 
105
 
106
+ # ───────────────────────────── Frame Processing for Tab 2 (Analysis-Only) - UPDATED
 
107
  def process_for_stats_only(frame):
108
  """
109
  Processes a frame but does not return any video/image output.
 
114
 
115
  t0 = time.perf_counter()
116
  try:
117
+ # --- FIX: Call with draw_visuals=False. The first returned value will be None. ---
118
+ _, indic = detector.process_frame(frame, draw_visuals=False)
119
  except Exception as e:
120
  logging.error(f"Error processing frame: {e}")
121
  indic = {"drowsiness_level": "Error", "lighting": "Unknown", "details": {"Score": 0.0}}
 
138
 
139
  return status_txt, audio_out
140
 
141
+
142
+ # ───────────────────────────── UI Definition (Unchanged)
143
  def create_readme_tab():
144
  """Creates the content for the 'About' tab."""
145
+ gr.Markdown(
146
+ """
147
+ <div align="center">
148
+ <img src="https://em-content.zobj.net/source/samsung/380/automobile_1f697.png" alt="Car Emoji" width="100"/>
149
+ <h1>Drive Paddy (Gradio Edition)</h1>
150
+ <p><strong>Your AI-Powered Drowsiness Detection Assistant</strong></p>
151
+ </div>
152
+
153
+ ---
154
+
155
+ ## 🌟 Features
156
+ - **Real-Time Webcam Streaming**: Directly processes your live camera feed for immediate feedback.
157
+ - **Efficient Geometric Analysis**: Uses `MediaPipe` for high-performance facial landmark detection.
158
+ - **Multi-Signal Analysis**: Detects eye closure (EAR), yawns (MAR), and head-nodding.
159
+ - **Stateful Alert System**: Plays a clear audio alert for new drowsiness events and intelligently re-arms itself, preventing alert fatigue.
160
+ - **Low-Light Warning**: Automatically detects and warns about poor lighting conditions.
161
+ - **Configurable**: Key detection thresholds and settings can be tuned via `config.yaml`.
162
+
163
+ ---
164
+
165
+ ## πŸ› οΈ How It Works
166
+ 1. **Video Streaming**: The `gradio.Image` component captures the camera feed.
167
+ 2. **Frame Processing**: Each frame is sent to the `GeometricProcessor`.
168
+ 3. **Stateful Alerting**: The `AlertManager` class uses internal state to decide if a *new* alert should be triggered.
169
+ 4. **Dynamic Updates**: The processed video, status text, and audio alerts are sent back to the frontend for a seamless real-time experience.
170
+
171
+ ---
172
+
173
+ ## πŸ’‘ Understanding the Live Status
174
+ The status panel provides real-time feedback on the following parameters:
175
+
176
+ - **`Lighting`**: Indicates the ambient light conditions.
177
+ - `Good`: Sufficient light for reliable detection.
178
+ - `Low`: Insufficient light. Detection is paused as the results would be unreliable.
179
+
180
+ - **`Status`**: The overall assessed level of driver alertness.
181
+ - `Awake`: The driver appears alert.
182
+ - `Slightly Drowsy`: Early signs of fatigue have been detected.
183
+ - `Very Drowsy`: Strong indicators of drowsiness are present. An alert is triggered.
184
+
185
+ - **`Score`**: A numerical value representing the accumulated evidence of drowsiness based on the weighted indicators (eye closure, yawning, head pose). A higher score corresponds to a greater level of detected drowsiness.
186
+ """
187
+ )
 
 
188
 
 
 
189
  def create_detection_tab():
190
+ """Creates the content for the 'Live Detection' tab (Image Stream)."""
191
+ gr.Markdown("## πŸ“Ή Live Drowsiness Detection (Image Stream)")
192
+ gr.Markdown("This feed provides the lowest latency by streaming processed images directly.")
193
+ with gr.Row():
194
+ with gr.Column(scale=2):
195
+ cam = gr.Image(sources=["webcam"], streaming=True, label="Live Camera Feed")
196
+ with gr.Column(scale=1):
197
+ out_img = gr.Image(label="Processed Feed")
198
+ out_text = gr.Textbox(label="Live Status", lines=3, interactive=False)
199
+ out_audio = gr.Audio(label="Alert", autoplay=True, visible=False)
 
 
 
 
 
 
 
 
 
200
 
201
+ cam.stream(
202
+ fn=process_live_frame,
203
+ inputs=[cam],
204
+ outputs=[out_img, out_text, out_audio]
205
+ )
206
+
 
207
  def create_analysis_only_tab():
208
  """Creates the content for the Analysis-Only Mode tab."""
209
  gr.Markdown("## ⚑ Analysis-Only Mode")
 
223
  outputs=[out_text_analysis, out_audio_analysis]
224
  )
225
 
226
+
227
+ # --- Main App Interface with Tabs ---
228
+ with gr.Blocks(title="Drive Paddy – Drowsiness Detection", theme=gr.themes.Soft()) as app:
229
  gr.Markdown("# πŸš— **Drive Paddy**")
230
  with gr.Tabs():
231
  with gr.TabItem("Live Detection"):
 
236
  create_readme_tab()
237
 
238
  if __name__ == "__main__":
239
+ logging.info("Launching Gradio app...")
240
+ app.launch(debug=True)