aheedsajid commited on
Commit
1cd3ee6
Β·
verified Β·
1 Parent(s): a049c41

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +504 -0
  2. audio_utils.py +341 -0
  3. requirements.txt +3 -0
  4. start_server.py +75 -0
app.py ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Real-time Microphone Streaming Server with Gradio Interface
3
+ Hosted on Hugging Face Spaces with WebSocket support for Android devices
4
+ """
5
+
6
+ import gradio as gr
7
+ import asyncio
8
+ import websockets
9
+ import json
10
+ import logging
11
+ import threading
12
+ import time
13
+ import base64
14
+ from datetime import datetime
15
+ from typing import Dict, List, Optional
16
+ import queue
17
+ import numpy as np
18
+
19
+ from audio_utils import AudioStreamManager, AudioProcessor
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Global variables
26
+ stream_manager = AudioStreamManager()
27
+ websocket_server = None
28
+ connected_clients = {}
29
+ client_lock = threading.Lock()
30
+
31
+ # Message queues for real-time updates
32
+ device_update_queue = queue.Queue()
33
+ audio_data_queue = queue.Queue()
34
+
35
+ class GradioWebSocketHandler:
36
+ """Handles WebSocket connections from Android devices"""
37
+
38
+ def __init__(self):
39
+ self.running = False
40
+
41
+ async def handle_client(self, websocket, path):
42
+ """Handle individual WebSocket client connection"""
43
+ client_id = f"{websocket.remote_address[0]}:{websocket.remote_address[1]}"
44
+ client_type = None
45
+ device_id = None
46
+
47
+ logger.info(f"New WebSocket connection from {client_id}")
48
+
49
+ with client_lock:
50
+ connected_clients[client_id] = {
51
+ 'websocket': websocket,
52
+ 'connected_at': time.time(),
53
+ 'type': 'unknown'
54
+ }
55
+
56
+ try:
57
+ # Send welcome message
58
+ await websocket.send(json.dumps({
59
+ 'type': 'welcome',
60
+ 'message': 'Connected to Real-time Audio Streaming Server (Gradio)',
61
+ 'timestamp': datetime.now().isoformat(),
62
+ 'server_info': {
63
+ 'platform': 'Gradio/HuggingFace',
64
+ 'version': '1.0.0'
65
+ }
66
+ }))
67
+
68
+ async for message in websocket:
69
+ try:
70
+ data = json.loads(message)
71
+ message_type = data.get('type', '')
72
+
73
+ if message_type == 'register_device':
74
+ # Android device registration
75
+ client_type = 'device'
76
+ device_id = data.get('device_id', '')
77
+ device_name = data.get('device_name', 'Unknown Device')
78
+ device_model = data.get('device_model', 'Unknown')
79
+
80
+ # Register device with stream manager
81
+ device_info = {
82
+ 'name': device_name,
83
+ 'model': device_model,
84
+ 'client_id': client_id,
85
+ 'platform': 'Android'
86
+ }
87
+
88
+ stream_manager.device_manager.register_device(device_id, device_info)
89
+
90
+ with client_lock:
91
+ connected_clients[client_id]['type'] = 'device'
92
+ connected_clients[client_id]['device_id'] = device_id
93
+
94
+ # Send registration confirmation
95
+ await websocket.send(json.dumps({
96
+ 'type': 'registration_success',
97
+ 'device_id': device_id,
98
+ 'message': 'Device registered successfully',
99
+ 'timestamp': datetime.now().isoformat()
100
+ }))
101
+
102
+ # Notify Gradio interface
103
+ device_update_queue.put({
104
+ 'type': 'device_connected',
105
+ 'device_id': device_id,
106
+ 'device_info': device_info
107
+ })
108
+
109
+ logger.info(f"Device registered: {device_id} ({device_name})")
110
+
111
+ elif message_type == 'audio_chunk':
112
+ # Audio data from Android device
113
+ if client_type == 'device' and device_id:
114
+ audio_data = data.get('data', {})
115
+
116
+ # Decode base64 audio
117
+ base64_audio = audio_data.get('audio_data', '')
118
+ if base64_audio:
119
+ audio_bytes = AudioProcessor.decode_base64_audio(base64_audio)
120
+ if audio_bytes:
121
+ timestamp = data.get('timestamp', time.time())
122
+
123
+ # Add to stream manager
124
+ stream_manager.device_manager.add_audio_chunk(
125
+ device_id, audio_bytes, timestamp
126
+ )
127
+
128
+ # Queue for Gradio display
129
+ audio_data_queue.put({
130
+ 'device_id': device_id,
131
+ 'timestamp': timestamp,
132
+ 'size_bytes': len(audio_bytes),
133
+ 'sample_rate': audio_data.get('sample_rate', 16000)
134
+ })
135
+
136
+ elif message_type == 'start_streaming':
137
+ if client_type == 'device' and device_id:
138
+ stream_manager.start_stream(device_id)
139
+ await websocket.send(json.dumps({
140
+ 'type': 'streaming_started',
141
+ 'device_id': device_id,
142
+ 'timestamp': datetime.now().isoformat()
143
+ }))
144
+
145
+ elif message_type == 'stop_streaming':
146
+ if client_type == 'device' and device_id:
147
+ stream_manager.stop_stream(device_id)
148
+ await websocket.send(json.dumps({
149
+ 'type': 'streaming_stopped',
150
+ 'device_id': device_id,
151
+ 'timestamp': datetime.now().isoformat()
152
+ }))
153
+
154
+ elif message_type == 'ping':
155
+ # Heartbeat response
156
+ if device_id:
157
+ stream_manager.device_manager.update_device_heartbeat(device_id)
158
+
159
+ await websocket.send(json.dumps({
160
+ 'type': 'pong',
161
+ 'timestamp': datetime.now().isoformat()
162
+ }))
163
+
164
+ else:
165
+ logger.warning(f"Unknown message type: {message_type}")
166
+
167
+ except json.JSONDecodeError as e:
168
+ logger.error(f"JSON decode error: {e}")
169
+ except Exception as e:
170
+ logger.error(f"Error processing message: {e}")
171
+
172
+ except websockets.exceptions.ConnectionClosed:
173
+ logger.info(f"WebSocket connection closed: {client_id}")
174
+ except Exception as e:
175
+ logger.error(f"WebSocket error: {e}")
176
+ finally:
177
+ # Cleanup on disconnect
178
+ with client_lock:
179
+ if client_id in connected_clients:
180
+ client_info = connected_clients[client_id]
181
+ if client_info.get('type') == 'device' and 'device_id' in client_info:
182
+ device_id = client_info['device_id']
183
+ stream_manager.device_manager.unregister_device(device_id)
184
+
185
+ # Notify Gradio interface
186
+ device_update_queue.put({
187
+ 'type': 'device_disconnected',
188
+ 'device_id': device_id
189
+ })
190
+
191
+ logger.info(f"Device disconnected: {device_id}")
192
+
193
+ del connected_clients[client_id]
194
+
195
+ async def send_command_to_device(self, device_id: str, command: str) -> bool:
196
+ """Send command to specific device"""
197
+ with client_lock:
198
+ for client_id, client_info in connected_clients.items():
199
+ if (client_info.get('type') == 'device' and
200
+ client_info.get('device_id') == device_id):
201
+ try:
202
+ await client_info['websocket'].send(json.dumps({
203
+ 'type': command,
204
+ 'timestamp': datetime.now().isoformat()
205
+ }))
206
+ return True
207
+ except Exception as e:
208
+ logger.error(f"Error sending command to device {device_id}: {e}")
209
+ return False
210
+ return False
211
+
212
+ # WebSocket handler instance
213
+ ws_handler = GradioWebSocketHandler()
214
+
215
+ def start_websocket_server():
216
+ """Start WebSocket server in background thread"""
217
+ global websocket_server
218
+
219
+ async def run_server():
220
+ global websocket_server
221
+ # Use port 7861 for WebSocket (Gradio uses 7860)
222
+ websocket_server = await websockets.serve(
223
+ ws_handler.handle_client,
224
+ "0.0.0.0",
225
+ 7861,
226
+ ping_interval=30,
227
+ ping_timeout=60
228
+ )
229
+ logger.info("WebSocket server started on port 7861")
230
+ await websocket_server.wait_closed()
231
+
232
+ def run_in_thread():
233
+ loop = asyncio.new_event_loop()
234
+ asyncio.set_event_loop(loop)
235
+ loop.run_until_complete(run_server())
236
+
237
+ server_thread = threading.Thread(target=run_in_thread, daemon=True)
238
+ server_thread.start()
239
+
240
+ def get_connected_devices() -> List[Dict]:
241
+ """Get list of connected devices for Gradio display"""
242
+ devices = stream_manager.device_manager.get_all_devices_info()
243
+ device_list = []
244
+
245
+ for device_id, device_info in devices.items():
246
+ if device_info.get('status') == 'connected':
247
+ device_data = {
248
+ 'device_id': device_id,
249
+ 'name': device_info.get('info', {}).get('name', 'Unknown'),
250
+ 'model': device_info.get('info', {}).get('model', 'Unknown'),
251
+ 'connected_at': datetime.fromtimestamp(device_info.get('connected_at', 0)).strftime('%H:%M:%S'),
252
+ 'streaming': device_info.get('streaming', False),
253
+ 'total_chunks': device_info.get('stats', {}).get('total_chunks', 0),
254
+ 'bytes_received': device_info.get('stats', {}).get('bytes_received', 0),
255
+ 'last_seen': 'Now' if time.time() - device_info.get('last_seen', 0) < 10 else f"{int(time.time() - device_info.get('last_seen', 0))}s ago"
256
+ }
257
+ device_list.append(device_data)
258
+
259
+ return device_list
260
+
261
+ def start_streaming(device_id: str) -> str:
262
+ """Start streaming from a device"""
263
+ if stream_manager.start_stream(device_id):
264
+ # Send command to device
265
+ asyncio.run(ws_handler.send_command_to_device(device_id, 'start_streaming'))
266
+ return f"βœ… Started streaming from device: {device_id}"
267
+ else:
268
+ return f"❌ Failed to start streaming from device: {device_id}"
269
+
270
+ def stop_streaming(device_id: str) -> str:
271
+ """Stop streaming from a device"""
272
+ if stream_manager.stop_stream(device_id):
273
+ # Send command to device
274
+ asyncio.run(ws_handler.send_command_to_device(device_id, 'stop_streaming'))
275
+ return f"πŸ›‘ Stopped streaming from device: {device_id}"
276
+ else:
277
+ return f"❌ Failed to stop streaming from device: {device_id}"
278
+
279
+ def get_server_stats() -> Dict:
280
+ """Get server statistics"""
281
+ devices = stream_manager.device_manager.get_all_devices_info()
282
+ connected_count = len([d for d in devices.values() if d.get('status') == 'connected'])
283
+ streaming_count = len(stream_manager.get_streaming_devices())
284
+
285
+ total_bytes = sum([
286
+ device.get('stats', {}).get('bytes_received', 0)
287
+ for device in devices.values()
288
+ ])
289
+
290
+ with client_lock:
291
+ total_connections = len(connected_clients)
292
+
293
+ return {
294
+ 'connected_devices': connected_count,
295
+ 'streaming_devices': streaming_count,
296
+ 'total_connections': total_connections,
297
+ 'total_data_mb': round(total_bytes / (1024 * 1024), 2),
298
+ 'server_uptime': time.strftime('%H:%M:%S', time.gmtime(time.time() - server_start_time))
299
+ }
300
+
301
+ def update_device_list():
302
+ """Update device list for Gradio"""
303
+ devices = get_connected_devices()
304
+ if not devices:
305
+ return "No devices connected. Install the Android app and connect to start streaming."
306
+
307
+ # Create HTML table for devices
308
+ html = """
309
+ <div style="font-family: Arial, sans-serif;">
310
+ <h3>πŸ“± Connected Devices</h3>
311
+ <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
312
+ <thead>
313
+ <tr style="background-color: #f0f0f0;">
314
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Device</th>
315
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Status</th>
316
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Data</th>
317
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Last Seen</th>
318
+ </tr>
319
+ </thead>
320
+ <tbody>
321
+ """
322
+
323
+ for device in devices:
324
+ status_icon = "πŸŽ™οΈ Streaming" if device['streaming'] else "βœ… Connected"
325
+ status_color = "#28a745" if device['streaming'] else "#007bff"
326
+
327
+ html += f"""
328
+ <tr>
329
+ <td style="border: 1px solid #ddd; padding: 8px;">
330
+ <strong>{device['name']}</strong><br>
331
+ <small style="color: #666;">{device['device_id'][:12]}...</small>
332
+ </td>
333
+ <td style="border: 1px solid #ddd; padding: 8px;">
334
+ <span style="color: {status_color}; font-weight: bold;">{status_icon}</span><br>
335
+ <small>Since: {device['connected_at']}</small>
336
+ </td>
337
+ <td style="border: 1px solid #ddd; padding: 8px;">
338
+ <strong>{device['total_chunks']}</strong> chunks<br>
339
+ <small>{round(device['bytes_received']/1024, 1)} KB</small>
340
+ </td>
341
+ <td style="border: 1px solid #ddd; padding: 8px;">
342
+ {device['last_seen']}
343
+ </td>
344
+ </tr>
345
+ """
346
+
347
+ html += """
348
+ </tbody>
349
+ </table>
350
+ </div>
351
+ """
352
+
353
+ return html
354
+
355
+ def create_gradio_interface():
356
+ """Create the main Gradio interface"""
357
+
358
+ with gr.Blocks(title="🎀 Real-time Mic Streaming Server", theme=gr.themes.Soft()) as interface:
359
+
360
+ # Header
361
+ gr.HTML("""
362
+ <div style="text-align: center; padding: 20px; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px; margin-bottom: 20px;">
363
+ <h1 style="margin: 0; font-size: 2.5em;">🎀 Real-time Microphone Streaming</h1>
364
+ <p style="margin: 10px 0 0 0; font-size: 1.2em;">Android Device Audio Streaming Server</p>
365
+ </div>
366
+ """)
367
+
368
+ # Connection info
369
+ with gr.Row():
370
+ gr.HTML("""
371
+ <div style="background: #e8f4f8; padding: 15px; border-radius: 8px; border-left: 5px solid #007bff;">
372
+ <h3 style="margin: 0 0 10px 0;">πŸ“‘ Connection Information</h3>
373
+ <p style="margin: 5px 0;"><strong>WebSocket URL:</strong> <code>wss://your-space-name.hf.space:7861</code></p>
374
+ <p style="margin: 5px 0;"><strong>Protocol:</strong> WebSocket with JSON messages</p>
375
+ <p style="margin: 5px 0;">Configure your Android app to connect to this WebSocket URL.</p>
376
+ </div>
377
+ """)
378
+
379
+ # Server Statistics
380
+ with gr.Row():
381
+ with gr.Column(scale=1):
382
+ connected_devices_display = gr.Number(label="πŸ“± Connected Devices", value=0, interactive=False)
383
+ with gr.Column(scale=1):
384
+ streaming_devices_display = gr.Number(label="πŸŽ™οΈ Streaming Devices", value=0, interactive=False)
385
+ with gr.Column(scale=1):
386
+ total_data_display = gr.Number(label="πŸ“Š Total Data (MB)", value=0.0, interactive=False)
387
+ with gr.Column(scale=1):
388
+ uptime_display = gr.Textbox(label="⏱️ Server Uptime", value="00:00:00", interactive=False)
389
+
390
+ # Device List
391
+ device_list_display = gr.HTML(
392
+ value="No devices connected. Install the Android app and connect to start streaming.",
393
+ label="Connected Devices"
394
+ )
395
+
396
+ # Control Buttons
397
+ with gr.Row():
398
+ with gr.Column(scale=1):
399
+ device_id_input = gr.Textbox(
400
+ label="Device ID",
401
+ placeholder="Enter device ID to control",
402
+ info="Copy device ID from the table above"
403
+ )
404
+ with gr.Column(scale=1):
405
+ start_btn = gr.Button("🎀 Start Streaming", variant="primary")
406
+ stop_btn = gr.Button("πŸ›‘ Stop Streaming", variant="secondary")
407
+
408
+ # Control feedback
409
+ control_output = gr.Textbox(label="Control Status", interactive=False)
410
+
411
+ # Audio Data Monitor (recent activity)
412
+ audio_monitor = gr.HTML(
413
+ value="<p>No recent audio data...</p>",
414
+ label="Recent Audio Activity"
415
+ )
416
+
417
+ # Event handlers
418
+ start_btn.click(
419
+ fn=start_streaming,
420
+ inputs=[device_id_input],
421
+ outputs=[control_output]
422
+ )
423
+
424
+ stop_btn.click(
425
+ fn=stop_streaming,
426
+ inputs=[device_id_input],
427
+ outputs=[control_output]
428
+ )
429
+
430
+ # Auto-refresh components
431
+ def update_all():
432
+ stats = get_server_stats()
433
+ devices_html = update_device_list()
434
+
435
+ # Get recent audio activity
436
+ recent_audio = []
437
+ while not audio_data_queue.empty():
438
+ try:
439
+ audio_info = audio_data_queue.get_nowait()
440
+ recent_audio.append(audio_info)
441
+ except queue.Empty:
442
+ break
443
+
444
+ if recent_audio:
445
+ audio_html = "<div style='font-family: monospace; font-size: 12px;'>"
446
+ audio_html += "<h4>Recent Audio Chunks:</h4>"
447
+ for audio in recent_audio[-10:]: # Show last 10 chunks
448
+ timestamp = datetime.fromtimestamp(audio['timestamp']).strftime('%H:%M:%S')
449
+ audio_html += f"<p>{timestamp} - Device: {audio['device_id'][:12]}... | Size: {audio['size_bytes']} bytes | Rate: {audio['sample_rate']} Hz</p>"
450
+ audio_html += "</div>"
451
+ else:
452
+ audio_html = "<p>No recent audio data...</p>"
453
+
454
+ return (
455
+ stats['connected_devices'],
456
+ stats['streaming_devices'],
457
+ stats['total_data_mb'],
458
+ stats['server_uptime'],
459
+ devices_html,
460
+ audio_html
461
+ )
462
+
463
+ # Set up auto-refresh every 2 seconds
464
+ interface.load(
465
+ fn=update_all,
466
+ outputs=[
467
+ connected_devices_display,
468
+ streaming_devices_display,
469
+ total_data_display,
470
+ uptime_display,
471
+ device_list_display,
472
+ audio_monitor
473
+ ],
474
+ every=2
475
+ )
476
+
477
+ return interface
478
+
479
+ # Global server start time
480
+ server_start_time = time.time()
481
+
482
+ def main():
483
+ """Main application entry point"""
484
+ logger.info("Starting Real-time Microphone Streaming Server...")
485
+
486
+ # Start WebSocket server
487
+ start_websocket_server()
488
+
489
+ # Wait a moment for WebSocket server to start
490
+ time.sleep(2)
491
+
492
+ # Create and launch Gradio interface
493
+ interface = create_gradio_interface()
494
+
495
+ # Launch with public access for Hugging Face
496
+ interface.launch(
497
+ server_name="0.0.0.0",
498
+ server_port=7860,
499
+ share=False, # Set to True for public tunnels during development
500
+ debug=False
501
+ )
502
+
503
+ if __name__ == "__main__":
504
+ main()
audio_utils.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio Processing Utilities for Real-time Microphone Streaming
3
+ Handles audio data encoding, decoding, and processing for Gradio backend.
4
+ """
5
+
6
+ import base64
7
+ import numpy as np
8
+ import threading
9
+ import queue
10
+ import time
11
+ import logging
12
+ from datetime import datetime
13
+ from typing import Optional, Dict, List, Tuple
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class AudioBuffer:
18
+ """Thread-safe audio buffer for storing audio chunks"""
19
+
20
+ def __init__(self, max_size: int = 1000):
21
+ self.max_size = max_size
22
+ self.buffer = queue.Queue(maxsize=max_size)
23
+ self.lock = threading.Lock()
24
+ self._stats = {
25
+ 'total_chunks': 0,
26
+ 'dropped_chunks': 0,
27
+ 'buffer_size': 0
28
+ }
29
+
30
+ def add_chunk(self, audio_data: bytes, device_id: str, timestamp: float) -> bool:
31
+ """Add audio chunk to buffer. Returns True if successful, False if buffer full"""
32
+ try:
33
+ chunk = {
34
+ 'audio_data': audio_data,
35
+ 'device_id': device_id,
36
+ 'timestamp': timestamp,
37
+ 'server_received': time.time()
38
+ }
39
+
40
+ self.buffer.put_nowait(chunk)
41
+ with self.lock:
42
+ self._stats['total_chunks'] += 1
43
+ self._stats['buffer_size'] = self.buffer.qsize()
44
+ return True
45
+
46
+ except queue.Full:
47
+ with self.lock:
48
+ self._stats['dropped_chunks'] += 1
49
+ logger.warning(f"Audio buffer full, dropped chunk from device {device_id}")
50
+ return False
51
+
52
+ def get_chunk(self, timeout: float = 0.1) -> Optional[Dict]:
53
+ """Get next audio chunk from buffer"""
54
+ try:
55
+ chunk = self.buffer.get(timeout=timeout)
56
+ with self.lock:
57
+ self._stats['buffer_size'] = self.buffer.qsize()
58
+ return chunk
59
+ except queue.Empty:
60
+ return None
61
+
62
+ def get_stats(self) -> Dict:
63
+ """Get buffer statistics"""
64
+ with self.lock:
65
+ return self._stats.copy()
66
+
67
+ def clear(self):
68
+ """Clear the buffer"""
69
+ with self.lock:
70
+ while not self.buffer.empty():
71
+ try:
72
+ self.buffer.get_nowait()
73
+ except queue.Empty:
74
+ break
75
+ self._stats['buffer_size'] = 0
76
+
77
+ class AudioProcessor:
78
+ """Audio processing utilities"""
79
+
80
+ @staticmethod
81
+ def decode_base64_audio(base64_data: str) -> Optional[bytes]:
82
+ """Decode base64 audio data to bytes"""
83
+ try:
84
+ # Clean the base64 string
85
+ clean_base64 = base64_data.replace('\n', '').replace('\r', '').strip()
86
+
87
+ # Add padding if necessary
88
+ missing_padding = len(clean_base64) % 4
89
+ if missing_padding:
90
+ clean_base64 += '=' * (4 - missing_padding)
91
+
92
+ # Decode base64 to bytes
93
+ audio_bytes = base64.b64decode(clean_base64)
94
+ return audio_bytes
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error decoding base64 audio: {e}")
98
+ return None
99
+
100
+ @staticmethod
101
+ def encode_audio_to_base64(audio_bytes: bytes) -> str:
102
+ """Encode audio bytes to base64 string"""
103
+ return base64.b64encode(audio_bytes).decode('utf-8')
104
+
105
+ @staticmethod
106
+ def pcm_to_numpy(audio_bytes: bytes, sample_rate: int = 16000, channels: int = 1) -> Optional[np.ndarray]:
107
+ """Convert PCM bytes to numpy array"""
108
+ try:
109
+ # Convert bytes to 16-bit signed integers
110
+ audio_array = np.frombuffer(audio_bytes, dtype=np.int16)
111
+
112
+ # Normalize to float32 range [-1.0, 1.0]
113
+ audio_float = audio_array.astype(np.float32) / 32768.0
114
+
115
+ # Handle multi-channel audio
116
+ if channels > 1:
117
+ audio_float = audio_float.reshape(-1, channels)
118
+
119
+ return audio_float
120
+
121
+ except Exception as e:
122
+ logger.error(f"Error converting PCM to numpy: {e}")
123
+ return None
124
+
125
+ @staticmethod
126
+ def numpy_to_pcm(audio_array: np.ndarray) -> bytes:
127
+ """Convert numpy array to PCM bytes"""
128
+ try:
129
+ # Ensure array is float32
130
+ if audio_array.dtype != np.float32:
131
+ audio_array = audio_array.astype(np.float32)
132
+
133
+ # Clamp values to [-1.0, 1.0]
134
+ audio_array = np.clip(audio_array, -1.0, 1.0)
135
+
136
+ # Convert to 16-bit signed integers
137
+ audio_int16 = (audio_array * 32767).astype(np.int16)
138
+
139
+ # Convert to bytes
140
+ return audio_int16.tobytes()
141
+
142
+ except Exception as e:
143
+ logger.error(f"Error converting numpy to PCM: {e}")
144
+ return b''
145
+
146
+ @staticmethod
147
+ def calculate_audio_stats(audio_bytes: bytes) -> Dict:
148
+ """Calculate basic audio statistics"""
149
+ try:
150
+ audio_array = np.frombuffer(audio_bytes, dtype=np.int16)
151
+ audio_float = audio_array.astype(np.float32) / 32768.0
152
+
153
+ stats = {
154
+ 'length_samples': len(audio_array),
155
+ 'length_bytes': len(audio_bytes),
156
+ 'rms': float(np.sqrt(np.mean(audio_float**2))),
157
+ 'peak': float(np.max(np.abs(audio_float))),
158
+ 'mean': float(np.mean(audio_float)),
159
+ 'std': float(np.std(audio_float))
160
+ }
161
+ return stats
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error calculating audio stats: {e}")
165
+ return {}
166
+
167
+ class DeviceManager:
168
+ """Manages connected audio streaming devices"""
169
+
170
+ def __init__(self):
171
+ self.devices = {}
172
+ self.lock = threading.Lock()
173
+ self.device_stats = {}
174
+
175
+ def register_device(self, device_id: str, device_info: Dict) -> bool:
176
+ """Register a new device"""
177
+ with self.lock:
178
+ self.devices[device_id] = {
179
+ 'info': device_info,
180
+ 'connected_at': time.time(),
181
+ 'last_seen': time.time(),
182
+ 'status': 'connected',
183
+ 'streaming': False,
184
+ 'audio_buffer': AudioBuffer()
185
+ }
186
+ self.device_stats[device_id] = {
187
+ 'total_chunks': 0,
188
+ 'bytes_received': 0,
189
+ 'connection_time': time.time(),
190
+ 'last_chunk_time': None
191
+ }
192
+
193
+ logger.info(f"Device registered: {device_id} ({device_info.get('name', 'Unknown')})")
194
+ return True
195
+
196
+ def unregister_device(self, device_id: str) -> bool:
197
+ """Unregister a device"""
198
+ with self.lock:
199
+ if device_id in self.devices:
200
+ self.devices[device_id]['status'] = 'disconnected'
201
+ self.devices[device_id]['disconnected_at'] = time.time()
202
+ # Clear the audio buffer
203
+ self.devices[device_id]['audio_buffer'].clear()
204
+
205
+ logger.info(f"Device unregistered: {device_id}")
206
+ return True
207
+
208
+ def update_device_heartbeat(self, device_id: str):
209
+ """Update device last seen timestamp"""
210
+ with self.lock:
211
+ if device_id in self.devices:
212
+ self.devices[device_id]['last_seen'] = time.time()
213
+
214
+ def add_audio_chunk(self, device_id: str, audio_data: bytes, timestamp: float) -> bool:
215
+ """Add audio chunk for a device"""
216
+ with self.lock:
217
+ if device_id not in self.devices:
218
+ return False
219
+
220
+ device = self.devices[device_id]
221
+ if device['status'] != 'connected':
222
+ return False
223
+
224
+ # Update device streaming status
225
+ device['streaming'] = True
226
+ device['last_seen'] = time.time()
227
+
228
+ # Update stats
229
+ stats = self.device_stats[device_id]
230
+ stats['total_chunks'] += 1
231
+ stats['bytes_received'] += len(audio_data)
232
+ stats['last_chunk_time'] = time.time()
233
+
234
+ # Add to buffer
235
+ return device['audio_buffer'].add_chunk(audio_data, device_id, timestamp)
236
+
237
+ def get_audio_chunk(self, device_id: str) -> Optional[Dict]:
238
+ """Get next audio chunk from device"""
239
+ with self.lock:
240
+ if device_id not in self.devices:
241
+ return None
242
+ return self.devices[device_id]['audio_buffer'].get_chunk()
243
+
244
+ def get_connected_devices(self) -> List[str]:
245
+ """Get list of connected device IDs"""
246
+ with self.lock:
247
+ return [
248
+ device_id for device_id, device in self.devices.items()
249
+ if device['status'] == 'connected'
250
+ ]
251
+
252
+ def get_device_info(self, device_id: str) -> Optional[Dict]:
253
+ """Get device information"""
254
+ with self.lock:
255
+ if device_id in self.devices:
256
+ device = self.devices[device_id].copy()
257
+ device['stats'] = self.device_stats[device_id].copy()
258
+ return device
259
+ return None
260
+
261
+ def get_all_devices_info(self) -> Dict:
262
+ """Get information for all devices"""
263
+ with self.lock:
264
+ devices_info = {}
265
+ for device_id, device in self.devices.items():
266
+ device_copy = device.copy()
267
+ device_copy['stats'] = self.device_stats[device_id].copy()
268
+ devices_info[device_id] = device_copy
269
+ return devices_info
270
+
271
+ def cleanup_inactive_devices(self, timeout_seconds: int = 300):
272
+ """Remove devices that haven't been seen for a while"""
273
+ current_time = time.time()
274
+ with self.lock:
275
+ inactive_devices = []
276
+ for device_id, device in self.devices.items():
277
+ if device['status'] == 'connected':
278
+ if current_time - device['last_seen'] > timeout_seconds:
279
+ inactive_devices.append(device_id)
280
+
281
+ for device_id in inactive_devices:
282
+ logger.info(f"Cleaning up inactive device: {device_id}")
283
+ self.unregister_device(device_id)
284
+
285
+ class AudioStreamManager:
286
+ """Manages real-time audio streaming between devices and clients"""
287
+
288
+ def __init__(self):
289
+ self.device_manager = DeviceManager()
290
+ self.active_streams = {}
291
+ self.stream_lock = threading.Lock()
292
+ self.running = False
293
+
294
+ # Start cleanup thread
295
+ self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
296
+ self.cleanup_thread.start()
297
+
298
+ def start_stream(self, device_id: str) -> bool:
299
+ """Start streaming from a device"""
300
+ with self.stream_lock:
301
+ if device_id in self.device_manager.get_connected_devices():
302
+ self.active_streams[device_id] = {
303
+ 'started_at': time.time(),
304
+ 'active': True
305
+ }
306
+ logger.info(f"Started streaming from device: {device_id}")
307
+ return True
308
+ return False
309
+
310
+ def stop_stream(self, device_id: str) -> bool:
311
+ """Stop streaming from a device"""
312
+ with self.stream_lock:
313
+ if device_id in self.active_streams:
314
+ self.active_streams[device_id]['active'] = False
315
+ del self.active_streams[device_id]
316
+ logger.info(f"Stopped streaming from device: {device_id}")
317
+ return True
318
+ return False
319
+
320
+ def is_streaming(self, device_id: str) -> bool:
321
+ """Check if device is currently streaming"""
322
+ with self.stream_lock:
323
+ return device_id in self.active_streams and self.active_streams[device_id]['active']
324
+
325
+ def get_streaming_devices(self) -> List[str]:
326
+ """Get list of currently streaming devices"""
327
+ with self.stream_lock:
328
+ return [
329
+ device_id for device_id, stream in self.active_streams.items()
330
+ if stream['active']
331
+ ]
332
+
333
+ def _cleanup_loop(self):
334
+ """Background cleanup loop"""
335
+ while True:
336
+ try:
337
+ self.device_manager.cleanup_inactive_devices()
338
+ time.sleep(30) # Run cleanup every 30 seconds
339
+ except Exception as e:
340
+ logger.error(f"Error in cleanup loop: {e}")
341
+ time.sleep(5)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.0.0
2
+ websockets>=12.0
3
+ numpy>=1.21.0
start_server.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Local startup script for the Real-time Microphone Streaming Server
4
+ Use this for local development and testing before deploying to Hugging Face
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import time
11
+
12
+ def check_dependencies():
13
+ """Check if required packages are installed"""
14
+ required_packages = ['gradio', 'websockets', 'numpy']
15
+ missing_packages = []
16
+
17
+ for package in required_packages:
18
+ try:
19
+ __import__(package)
20
+ except ImportError:
21
+ missing_packages.append(package)
22
+
23
+ if missing_packages:
24
+ print("❌ Missing required packages:")
25
+ for package in missing_packages:
26
+ print(f" - {package}")
27
+ print("\nπŸ“¦ Installing missing packages...")
28
+
29
+ try:
30
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + missing_packages)
31
+ print("βœ… Packages installed successfully!")
32
+ except subprocess.CalledProcessError as e:
33
+ print(f"❌ Failed to install packages: {e}")
34
+ return False
35
+
36
+ return True
37
+
38
+ def main():
39
+ """Main startup function"""
40
+ print("🎀 Real-time Microphone Streaming Server")
41
+ print("=" * 50)
42
+
43
+ # Check dependencies
44
+ print("πŸ” Checking dependencies...")
45
+ if not check_dependencies():
46
+ print("❌ Dependency check failed. Exiting.")
47
+ sys.exit(1)
48
+
49
+ print("βœ… Dependencies OK")
50
+
51
+ # Set environment variables for local development
52
+ os.environ['GRADIO_SERVER_NAME'] = '0.0.0.0'
53
+ os.environ['GRADIO_SERVER_PORT'] = '7860'
54
+
55
+ print("\nπŸš€ Starting server...")
56
+ print("πŸ“‘ Gradio interface will be available at: http://localhost:7860")
57
+ print("πŸ”Œ WebSocket server will be available at: ws://localhost:7861")
58
+ print("\nπŸ“± Update your Android app to use: ws://YOUR_LOCAL_IP:7861")
59
+ print(" (Replace YOUR_LOCAL_IP with your computer's IP address)")
60
+ print("\n⏹️ Press Ctrl+C to stop the server")
61
+ print("=" * 50)
62
+
63
+ # Import and run the main app
64
+ try:
65
+ from app import main as run_app
66
+ run_app()
67
+ except KeyboardInterrupt:
68
+ print("\n\nπŸ›‘ Server stopped by user")
69
+ except Exception as e:
70
+ print(f"\n❌ Server error: {e}")
71
+ import traceback
72
+ traceback.print_exc()
73
+
74
+ if __name__ == "__main__":
75
+ main()