ciyidogan commited on
Commit
ed5b329
·
verified ·
1 Parent(s): 1d231ce

Update chat_session/websocket_manager.py

Browse files
Files changed (1) hide show
  1. chat_session/websocket_manager.py +530 -522
chat_session/websocket_manager.py CHANGED
@@ -1,523 +1,531 @@
1
- """
2
- WebSocket Manager for Flare
3
- ===========================
4
- Manages WebSocket connections and message routing
5
- """
6
- import base64
7
- import struct
8
- import asyncio
9
- from typing import Dict, Optional, Set
10
- from fastapi import WebSocket, WebSocketDisconnect
11
- import json
12
- from datetime import datetime
13
- import traceback
14
-
15
- from .event_bus import EventBus, Event, EventType
16
- from utils.logger import log_info, log_error, log_debug, log_warning
17
-
18
-
19
- class WebSocketConnection:
20
- """Wrapper for WebSocket connection with metadata"""
21
-
22
- def __init__(self, websocket: WebSocket, session_id: str):
23
- self.websocket = websocket
24
- self.session_id = session_id
25
- self.connected_at = datetime.utcnow()
26
- self.last_activity = datetime.utcnow()
27
- self.is_active = True
28
-
29
- async def send_json(self, data: dict):
30
- """Send JSON data to client"""
31
- try:
32
- if self.is_active:
33
- await self.websocket.send_json(data)
34
- self.last_activity = datetime.utcnow()
35
- except Exception as e:
36
- log_error(
37
- f"❌ Failed to send message",
38
- session_id=self.session_id,
39
- error=str(e)
40
- )
41
- self.is_active = False
42
- raise
43
-
44
- async def receive_json(self) -> dict:
45
- """Receive JSON data from client"""
46
- try:
47
- data = await self.websocket.receive_json()
48
- self.last_activity = datetime.utcnow()
49
- return data
50
- except WebSocketDisconnect:
51
- self.is_active = False
52
- raise
53
- except Exception as e:
54
- log_error(
55
- f"❌ Failed to receive message",
56
- session_id=self.session_id,
57
- error=str(e)
58
- )
59
- self.is_active = False
60
- raise
61
-
62
- async def close(self):
63
- """Close the connection"""
64
- try:
65
- self.is_active = False
66
- await self.websocket.close()
67
- except:
68
- pass
69
-
70
-
71
- class WebSocketManager:
72
- """Manages WebSocket connections and routing"""
73
-
74
- def __init__(self, event_bus: EventBus):
75
- self.event_bus = event_bus
76
- self.connections: Dict[str, WebSocketConnection] = {}
77
- self.message_queues: Dict[str, asyncio.Queue] = {}
78
- self._setup_event_handlers()
79
-
80
- def _setup_event_handlers(self):
81
- """Subscribe to events that need to be sent to clients"""
82
- # State events
83
- self.event_bus.subscribe(EventType.STATE_TRANSITION, self._handle_state_transition)
84
-
85
- # STT events
86
- self.event_bus.subscribe(EventType.STT_READY, self._handle_stt_ready)
87
- self.event_bus.subscribe(EventType.STT_RESULT, self._handle_stt_result)
88
-
89
- # TTS events
90
- self.event_bus.subscribe(EventType.TTS_STARTED, self._handle_tts_started)
91
- self.event_bus.subscribe(EventType.TTS_CHUNK_READY, self._handle_tts_chunk)
92
- self.event_bus.subscribe(EventType.TTS_COMPLETED, self._handle_tts_completed)
93
-
94
- # LLM events
95
- self.event_bus.subscribe(EventType.LLM_RESPONSE_READY, self._handle_llm_response)
96
-
97
- # Error events
98
- self.event_bus.subscribe(EventType.RECOVERABLE_ERROR, self._handle_error)
99
- self.event_bus.subscribe(EventType.CRITICAL_ERROR, self._handle_error)
100
-
101
- async def connect(self, websocket: WebSocket, session_id: str):
102
- """Accept new WebSocket connection"""
103
- await websocket.accept()
104
-
105
- # Check for existing connection
106
- if session_id in self.connections:
107
- log_warning(
108
- f"⚠️ Existing connection for session, closing old one",
109
- session_id=session_id
110
- )
111
- await self.disconnect(session_id)
112
-
113
- # Create connection wrapper
114
- connection = WebSocketConnection(websocket, session_id)
115
- self.connections[session_id] = connection
116
-
117
- # Create message queue
118
- self.message_queues[session_id] = asyncio.Queue()
119
-
120
- log_info(
121
- f"✅ WebSocket connected",
122
- session_id=session_id,
123
- total_connections=len(self.connections)
124
- )
125
-
126
- # Publish connection event
127
- await self.event_bus.publish(Event(
128
- type=EventType.WEBSOCKET_CONNECTED,
129
- session_id=session_id,
130
- data={}
131
- ))
132
-
133
- async def disconnect(self, session_id: str):
134
- """Disconnect WebSocket connection"""
135
- connection = self.connections.get(session_id)
136
- if connection:
137
- await connection.close()
138
- del self.connections[session_id]
139
-
140
- # Remove message queue
141
- if session_id in self.message_queues:
142
- del self.message_queues[session_id]
143
-
144
- log_info(
145
- f"🔌 WebSocket disconnected",
146
- session_id=session_id,
147
- total_connections=len(self.connections)
148
- )
149
-
150
- # Publish disconnection event
151
- await self.event_bus.publish(Event(
152
- type=EventType.WEBSOCKET_DISCONNECTED,
153
- session_id=session_id,
154
- data={}
155
- ))
156
-
157
- async def handle_connection(self, websocket: WebSocket, session_id: str):
158
- """Handle WebSocket connection lifecycle"""
159
- try:
160
- # Connect
161
- await self.connect(websocket, session_id)
162
-
163
- # Create tasks for bidirectional communication
164
- receive_task = asyncio.create_task(self._receive_messages(session_id))
165
- send_task = asyncio.create_task(self._send_messages(session_id))
166
-
167
- # Wait for either task to complete
168
- done, pending = await asyncio.wait(
169
- [receive_task, send_task],
170
- return_when=asyncio.FIRST_COMPLETED
171
- )
172
-
173
- # Cancel pending tasks
174
- for task in pending:
175
- task.cancel()
176
- try:
177
- await task
178
- except asyncio.CancelledError:
179
- pass
180
-
181
- except WebSocketDisconnect:
182
- log_info(f"WebSocket disconnected normally", session_id=session_id)
183
- except Exception as e:
184
- log_error(
185
- f"❌ WebSocket error",
186
- session_id=session_id,
187
- error=str(e),
188
- traceback=traceback.format_exc()
189
- )
190
-
191
- # Publish error event
192
- await self.event_bus.publish(Event(
193
- type=EventType.WEBSOCKET_ERROR,
194
- session_id=session_id,
195
- data={
196
- "error_type": "websocket_error",
197
- "message": str(e)
198
- }
199
- ))
200
- finally:
201
- # Ensure disconnection
202
- await self.disconnect(session_id)
203
-
204
- async def _receive_messages(self, session_id: str):
205
- """Receive messages from client"""
206
- connection = self.connections.get(session_id)
207
- if not connection:
208
- return
209
-
210
- try:
211
- while connection.is_active:
212
- # Receive message
213
- message = await connection.receive_json()
214
-
215
- log_debug(
216
- f"📨 Received message",
217
- session_id=session_id,
218
- message_type=message.get("type")
219
- )
220
-
221
- # Route message based on type
222
- await self._route_client_message(session_id, message)
223
-
224
- except WebSocketDisconnect:
225
- log_info(f"Client disconnected", session_id=session_id)
226
- except Exception as e:
227
- log_error(
228
- f"❌ Error receiving messages",
229
- session_id=session_id,
230
- error=str(e)
231
- )
232
- raise
233
-
234
- async def _send_messages(self, session_id: str):
235
- """Send queued messages to client"""
236
- connection = self.connections.get(session_id)
237
- queue = self.message_queues.get(session_id)
238
-
239
- if not connection or not queue:
240
- return
241
-
242
- try:
243
- while connection.is_active:
244
- # Wait for message with timeout
245
- try:
246
- message = await asyncio.wait_for(queue.get(), timeout=30.0)
247
-
248
- # Send to client
249
- await connection.send_json(message)
250
-
251
- log_debug(
252
- f"📤 Sent message",
253
- session_id=session_id,
254
- message_type=message.get("type")
255
- )
256
-
257
- except asyncio.TimeoutError:
258
- # Send ping to keep connection alive
259
- await connection.send_json({"type": "ping"})
260
-
261
- except Exception as e:
262
- log_error(
263
- f"❌ Error sending messages",
264
- session_id=session_id,
265
- error=str(e)
266
- )
267
- raise
268
-
269
- async def _route_client_message(self, session_id: str, message: dict):
270
- """Route message from client to appropriate handler"""
271
- message_type = message.get("type")
272
-
273
- if message_type == "audio_chunk":
274
- # Audio data from client
275
- audio_data_base64 = message.get("data")
276
-
277
- if audio_data_base64:
278
- # Debug için audio analizi
279
- try:
280
- import base64
281
- import struct
282
-
283
- # Base64'ten binary'ye çevir
284
- audio_data = base64.b64decode(audio_data_base64)
285
-
286
- # Session için debug counter
287
- if not hasattr(self, 'audio_debug_counters'):
288
- self.audio_debug_counters = {}
289
-
290
- if session_id not in self.audio_debug_counters:
291
- self.audio_debug_counters[session_id] = 0
292
-
293
- # İlk 5 chunk için detaylı log
294
- if self.audio_debug_counters[session_id] < 5:
295
- log_info(f"🔊 Audio chunk analysis #{self.audio_debug_counters[session_id]}",
296
- session_id=session_id,
297
- size_bytes=len(audio_data),
298
- base64_size=len(audio_data_base64))
299
-
300
- # İlk 20 byte'ı hex olarak göster
301
- if len(audio_data) >= 20:
302
- log_debug(f" First 20 bytes (hex): {audio_data[:20].hex()}")
303
-
304
- # Linear16 (little-endian int16) olarak yorumla
305
- samples = struct.unpack('<10h', audio_data[:20])
306
- log_debug(f" First 10 samples: {samples}")
307
- log_debug(f" Max amplitude (first 10): {max(abs(s) for s in samples)}")
308
-
309
- # Tüm chunk'ı analiz et
310
- total_samples = len(audio_data) // 2
311
- if total_samples > 0:
312
- all_samples = struct.unpack(f'<{total_samples}h', audio_data[:total_samples*2])
313
- max_amp = max(abs(s) for s in all_samples)
314
- avg_amp = sum(abs(s) for s in all_samples) / total_samples
315
-
316
- # Sessizlik kontrolü
317
- silent = max_amp < 100 # Linear16 için düşük eşik
318
-
319
- log_info(f" Audio stats - Max: {max_amp}, Avg: {avg_amp:.1f}, Silent: {silent}")
320
-
321
- # Eğer çok sessizse uyar
322
- if max_amp < 50:
323
- log_warning(f"⚠️ Very low audio level detected! Max amplitude: {max_amp}")
324
-
325
- self.audio_debug_counters[session_id] += 1
326
-
327
- except Exception as e:
328
- log_error(f"Error analyzing audio chunk: {e}")
329
-
330
- # Audio data from client
331
- await self.event_bus.publish(Event(
332
- type=EventType.AUDIO_CHUNK_RECEIVED,
333
- session_id=session_id,
334
- data={
335
- "audio_data": message.get("data"),
336
- "timestamp": message.get("timestamp")
337
- }
338
- ))
339
-
340
- elif message_type == "control":
341
- # Control messages
342
- action = message.get("action")
343
- config = message.get("config", {})
344
-
345
- if action == "start_conversation":
346
- # Yeni action: Mevcut session için conversation başlat
347
- log_info(f"🎤 Starting conversation for session | session_id={session_id}")
348
-
349
- await self.event_bus.publish(Event(
350
- type=EventType.CONVERSATION_STARTED,
351
- session_id=session_id,
352
- data={
353
- "config": config,
354
- "continuous_listening": config.get("continuous_listening", True)
355
- }
356
- ))
357
-
358
- # Send confirmation to client
359
- await self.send_message(session_id, {
360
- "type": "conversation_started",
361
- "message": "Conversation started successfully"
362
- })
363
-
364
- elif action == "stop_conversation":
365
- await self.event_bus.publish(Event(
366
- type=EventType.CONVERSATION_ENDED,
367
- session_id=session_id,
368
- data={"reason": "user_request"}
369
- ))
370
-
371
- elif action == "start_session":
372
- # Bu artık kullanılmamalı
373
- log_warning(f"⚠️ Deprecated start_session action received | session_id={session_id}")
374
-
375
- # Yine de işle ama conversation_started olarak
376
- await self.event_bus.publish(Event(
377
- type=EventType.CONVERSATION_STARTED,
378
- session_id=session_id,
379
- data=config
380
- ))
381
-
382
- elif action == "stop_session":
383
- await self.event_bus.publish(Event(
384
- type=EventType.CONVERSATION_ENDED,
385
- session_id=session_id,
386
- data={"reason": "user_request"}
387
- ))
388
-
389
- elif action == "end_session":
390
- await self.event_bus.publish(Event(
391
- type=EventType.SESSION_ENDED,
392
- session_id=session_id,
393
- data={"reason": "user_request"}
394
- ))
395
-
396
- elif action == "audio_ended":
397
- await self.event_bus.publish(Event(
398
- type=EventType.AUDIO_PLAYBACK_COMPLETED,
399
- session_id=session_id,
400
- data={}
401
- ))
402
-
403
- else:
404
- log_warning(
405
- f"⚠️ Unknown control action",
406
- session_id=session_id,
407
- action=action
408
- )
409
-
410
- elif message_type == "ping":
411
- # Respond to ping
412
- await self.send_message(session_id, {"type": "pong"})
413
-
414
- else:
415
- log_warning(
416
- f"⚠️ Unknown message type",
417
- session_id=session_id,
418
- message_type=message_type
419
- )
420
-
421
- async def send_message(self, session_id: str, message: dict):
422
- """Queue message for sending to client"""
423
- queue = self.message_queues.get(session_id)
424
- if queue:
425
- await queue.put(message)
426
- else:
427
- log_warning(
428
- f"⚠️ No queue for session",
429
- session_id=session_id
430
- )
431
-
432
- async def broadcast_to_session(self, session_id: str, message: dict):
433
- """Send message immediately (bypass queue)"""
434
- connection = self.connections.get(session_id)
435
- if connection and connection.is_active:
436
- await connection.send_json(message)
437
-
438
- # Event handlers for sending messages to clients
439
-
440
- async def _handle_state_transition(self, event: Event):
441
- """Send state transition to client"""
442
- await self.send_message(event.session_id, {
443
- "type": "state_change",
444
- "from": event.data.get("old_state"),
445
- "to": event.data.get("new_state")
446
- })
447
-
448
- async def _handle_stt_ready(self, event: Event):
449
- """Send STT ready signal to client"""
450
- await self.send_message(event.session_id, {
451
- "type": "stt_ready",
452
- "message": "STT is ready to receive audio"
453
- })
454
-
455
- async def _handle_stt_result(self, event: Event):
456
- """Send STT result to client"""
457
- # Her türlü result'ı (interim + final) frontend'e gönder
458
- await self.send_message(event.session_id, {
459
- "type": "transcription",
460
- "text": event.data.get("text", ""),
461
- "is_final": event.data.get("is_final", False),
462
- "confidence": event.data.get("confidence", 0.0)
463
- })
464
-
465
- async def _handle_tts_started(self, event: Event):
466
- """Send assistant message when TTS starts"""
467
- if event.data.get("is_welcome"):
468
- # Send welcome message to client
469
- await self.send_message(event.session_id, {
470
- "type": "assistant_response",
471
- "text": event.data.get("text", ""),
472
- "is_welcome": True
473
- })
474
-
475
- async def _handle_tts_chunk(self, event: Event):
476
- """Send TTS audio chunk to client"""
477
- await self.send_message(event.session_id, {
478
- "type": "tts_audio",
479
- "data": event.data.get("audio_data"),
480
- "chunk_index": event.data.get("chunk_index"),
481
- "total_chunks": event.data.get("total_chunks"),
482
- "is_last": event.data.get("is_last", False),
483
- "mime_type": event.data.get("mime_type", "audio/mpeg")
484
- })
485
-
486
- async def _handle_tts_completed(self, event: Event):
487
- """Notify client that TTS is complete"""
488
- # Client knows from is_last flag in chunks
489
- pass
490
-
491
- async def _handle_llm_response(self, event: Event):
492
- """Send LLM response to client"""
493
- await self.send_message(event.session_id, {
494
- "type": "assistant_response",
495
- "text": event.data.get("text", ""),
496
- "is_welcome": event.data.get("is_welcome", False)
497
- })
498
-
499
- async def _handle_error(self, event: Event):
500
- """Send error to client"""
501
- error_type = event.data.get("error_type", "unknown")
502
- message = event.data.get("message", "An error occurred")
503
-
504
- await self.send_message(event.session_id, {
505
- "type": "error",
506
- "error_type": error_type,
507
- "message": message,
508
- "details": event.data.get("details", {})
509
- })
510
-
511
- def get_connection_count(self) -> int:
512
- """Get number of active connections"""
513
- return len(self.connections)
514
-
515
- def get_session_connections(self) -> Set[str]:
516
- """Get all active session IDs"""
517
- return set(self.connections.keys())
518
-
519
- async def close_all_connections(self):
520
- """Close all active connections"""
521
- session_ids = list(self.connections.keys())
522
- for session_id in session_ids:
 
 
 
 
 
 
 
 
523
  await self.disconnect(session_id)
 
1
+ """
2
+ WebSocket Manager for Flare
3
+ ===========================
4
+ Manages WebSocket connections and message routing
5
+ """
6
+ import base64
7
+ import struct
8
+ import asyncio
9
+ from typing import Dict, Optional, Set
10
+ from fastapi import WebSocket, WebSocketDisconnect
11
+ import json
12
+ from datetime import datetime
13
+ import traceback
14
+
15
+ from .event_bus import EventBus, Event, EventType
16
+ from utils.logger import log_info, log_error, log_debug, log_warning
17
+
18
+
19
+ class WebSocketConnection:
20
+ """Wrapper for WebSocket connection with metadata"""
21
+
22
+ def __init__(self, websocket: WebSocket, session_id: str):
23
+ self.websocket = websocket
24
+ self.session_id = session_id
25
+ self.connected_at = datetime.utcnow()
26
+ self.last_activity = datetime.utcnow()
27
+ self.is_active = True
28
+
29
+ async def send_json(self, data: dict):
30
+ """Send JSON data to client"""
31
+ try:
32
+ if self.is_active:
33
+ await self.websocket.send_json(data)
34
+ self.last_activity = datetime.utcnow()
35
+ except Exception as e:
36
+ log_error(
37
+ f"❌ Failed to send message",
38
+ session_id=self.session_id,
39
+ error=str(e)
40
+ )
41
+ self.is_active = False
42
+ raise
43
+
44
+ async def receive_json(self) -> dict:
45
+ """Receive JSON data from client"""
46
+ try:
47
+ data = await self.websocket.receive_json()
48
+ self.last_activity = datetime.utcnow()
49
+ return data
50
+ except WebSocketDisconnect:
51
+ self.is_active = False
52
+ raise
53
+ except Exception as e:
54
+ log_error(
55
+ f"❌ Failed to receive message",
56
+ session_id=self.session_id,
57
+ error=str(e)
58
+ )
59
+ self.is_active = False
60
+ raise
61
+
62
+ async def close(self):
63
+ """Close the connection"""
64
+ try:
65
+ self.is_active = False
66
+ await self.websocket.close()
67
+ except:
68
+ pass
69
+
70
+
71
+ class WebSocketManager:
72
+ """Manages WebSocket connections and routing"""
73
+
74
+ def __init__(self, event_bus: EventBus):
75
+ self.event_bus = event_bus
76
+ self.connections: Dict[str, WebSocketConnection] = {}
77
+ self.message_queues: Dict[str, asyncio.Queue] = {}
78
+ self._setup_event_handlers()
79
+
80
+ def _setup_event_handlers(self):
81
+ """Subscribe to events that need to be sent to clients"""
82
+ # State events
83
+ self.event_bus.subscribe(EventType.STATE_TRANSITION, self._handle_state_transition)
84
+
85
+ # STT events
86
+ self.event_bus.subscribe(EventType.STT_READY, self._handle_stt_ready)
87
+ self.event_bus.subscribe(EventType.STT_RESULT, self._handle_stt_result)
88
+ self.event_bus.subscribe(EventType.STT_STOPPED, self._handle_stt_stopped)
89
+
90
+ # TTS events
91
+ self.event_bus.subscribe(EventType.TTS_STARTED, self._handle_tts_started)
92
+ self.event_bus.subscribe(EventType.TTS_CHUNK_READY, self._handle_tts_chunk)
93
+ self.event_bus.subscribe(EventType.TTS_COMPLETED, self._handle_tts_completed)
94
+
95
+ # LLM events
96
+ self.event_bus.subscribe(EventType.LLM_RESPONSE_READY, self._handle_llm_response)
97
+
98
+ # Error events
99
+ self.event_bus.subscribe(EventType.RECOVERABLE_ERROR, self._handle_error)
100
+ self.event_bus.subscribe(EventType.CRITICAL_ERROR, self._handle_error)
101
+
102
+ async def connect(self, websocket: WebSocket, session_id: str):
103
+ """Accept new WebSocket connection"""
104
+ await websocket.accept()
105
+
106
+ # Check for existing connection
107
+ if session_id in self.connections:
108
+ log_warning(
109
+ f"⚠️ Existing connection for session, closing old one",
110
+ session_id=session_id
111
+ )
112
+ await self.disconnect(session_id)
113
+
114
+ # Create connection wrapper
115
+ connection = WebSocketConnection(websocket, session_id)
116
+ self.connections[session_id] = connection
117
+
118
+ # Create message queue
119
+ self.message_queues[session_id] = asyncio.Queue()
120
+
121
+ log_info(
122
+ f"✅ WebSocket connected",
123
+ session_id=session_id,
124
+ total_connections=len(self.connections)
125
+ )
126
+
127
+ # Publish connection event
128
+ await self.event_bus.publish(Event(
129
+ type=EventType.WEBSOCKET_CONNECTED,
130
+ session_id=session_id,
131
+ data={}
132
+ ))
133
+
134
+ async def disconnect(self, session_id: str):
135
+ """Disconnect WebSocket connection"""
136
+ connection = self.connections.get(session_id)
137
+ if connection:
138
+ await connection.close()
139
+ del self.connections[session_id]
140
+
141
+ # Remove message queue
142
+ if session_id in self.message_queues:
143
+ del self.message_queues[session_id]
144
+
145
+ log_info(
146
+ f"🔌 WebSocket disconnected",
147
+ session_id=session_id,
148
+ total_connections=len(self.connections)
149
+ )
150
+
151
+ # Publish disconnection event
152
+ await self.event_bus.publish(Event(
153
+ type=EventType.WEBSOCKET_DISCONNECTED,
154
+ session_id=session_id,
155
+ data={}
156
+ ))
157
+
158
+ async def handle_connection(self, websocket: WebSocket, session_id: str):
159
+ """Handle WebSocket connection lifecycle"""
160
+ try:
161
+ # Connect
162
+ await self.connect(websocket, session_id)
163
+
164
+ # Create tasks for bidirectional communication
165
+ receive_task = asyncio.create_task(self._receive_messages(session_id))
166
+ send_task = asyncio.create_task(self._send_messages(session_id))
167
+
168
+ # Wait for either task to complete
169
+ done, pending = await asyncio.wait(
170
+ [receive_task, send_task],
171
+ return_when=asyncio.FIRST_COMPLETED
172
+ )
173
+
174
+ # Cancel pending tasks
175
+ for task in pending:
176
+ task.cancel()
177
+ try:
178
+ await task
179
+ except asyncio.CancelledError:
180
+ pass
181
+
182
+ except WebSocketDisconnect:
183
+ log_info(f"WebSocket disconnected normally", session_id=session_id)
184
+ except Exception as e:
185
+ log_error(
186
+ f"❌ WebSocket error",
187
+ session_id=session_id,
188
+ error=str(e),
189
+ traceback=traceback.format_exc()
190
+ )
191
+
192
+ # Publish error event
193
+ await self.event_bus.publish(Event(
194
+ type=EventType.WEBSOCKET_ERROR,
195
+ session_id=session_id,
196
+ data={
197
+ "error_type": "websocket_error",
198
+ "message": str(e)
199
+ }
200
+ ))
201
+ finally:
202
+ # Ensure disconnection
203
+ await self.disconnect(session_id)
204
+
205
+ async def _receive_messages(self, session_id: str):
206
+ """Receive messages from client"""
207
+ connection = self.connections.get(session_id)
208
+ if not connection:
209
+ return
210
+
211
+ try:
212
+ while connection.is_active:
213
+ # Receive message
214
+ message = await connection.receive_json()
215
+
216
+ log_debug(
217
+ f"📨 Received message",
218
+ session_id=session_id,
219
+ message_type=message.get("type")
220
+ )
221
+
222
+ # Route message based on type
223
+ await self._route_client_message(session_id, message)
224
+
225
+ except WebSocketDisconnect:
226
+ log_info(f"Client disconnected", session_id=session_id)
227
+ except Exception as e:
228
+ log_error(
229
+ f"❌ Error receiving messages",
230
+ session_id=session_id,
231
+ error=str(e)
232
+ )
233
+ raise
234
+
235
+ async def _send_messages(self, session_id: str):
236
+ """Send queued messages to client"""
237
+ connection = self.connections.get(session_id)
238
+ queue = self.message_queues.get(session_id)
239
+
240
+ if not connection or not queue:
241
+ return
242
+
243
+ try:
244
+ while connection.is_active:
245
+ # Wait for message with timeout
246
+ try:
247
+ message = await asyncio.wait_for(queue.get(), timeout=30.0)
248
+
249
+ # Send to client
250
+ await connection.send_json(message)
251
+
252
+ log_debug(
253
+ f"📤 Sent message",
254
+ session_id=session_id,
255
+ message_type=message.get("type")
256
+ )
257
+
258
+ except asyncio.TimeoutError:
259
+ # Send ping to keep connection alive
260
+ await connection.send_json({"type": "ping"})
261
+
262
+ except Exception as e:
263
+ log_error(
264
+ f"❌ Error sending messages",
265
+ session_id=session_id,
266
+ error=str(e)
267
+ )
268
+ raise
269
+
270
+ async def _route_client_message(self, session_id: str, message: dict):
271
+ """Route message from client to appropriate handler"""
272
+ message_type = message.get("type")
273
+
274
+ if message_type == "audio_chunk":
275
+ # Audio data from client
276
+ audio_data_base64 = message.get("data")
277
+
278
+ if audio_data_base64:
279
+ # Debug için audio analizi
280
+ try:
281
+ import base64
282
+ import struct
283
+
284
+ # Base64'ten binary'ye çevir
285
+ audio_data = base64.b64decode(audio_data_base64)
286
+
287
+ # Session için debug counter
288
+ if not hasattr(self, 'audio_debug_counters'):
289
+ self.audio_debug_counters = {}
290
+
291
+ if session_id not in self.audio_debug_counters:
292
+ self.audio_debug_counters[session_id] = 0
293
+
294
+ # İlk 5 chunk için detaylı log
295
+ if self.audio_debug_counters[session_id] < 5:
296
+ log_info(f"🔊 Audio chunk analysis #{self.audio_debug_counters[session_id]}",
297
+ session_id=session_id,
298
+ size_bytes=len(audio_data),
299
+ base64_size=len(audio_data_base64))
300
+
301
+ # İlk 20 byte'ı hex olarak göster
302
+ if len(audio_data) >= 20:
303
+ log_debug(f" First 20 bytes (hex): {audio_data[:20].hex()}")
304
+
305
+ # Linear16 (little-endian int16) olarak yorumla
306
+ samples = struct.unpack('<10h', audio_data[:20])
307
+ log_debug(f" First 10 samples: {samples}")
308
+ log_debug(f" Max amplitude (first 10): {max(abs(s) for s in samples)}")
309
+
310
+ # Tüm chunk'ı analiz et
311
+ total_samples = len(audio_data) // 2
312
+ if total_samples > 0:
313
+ all_samples = struct.unpack(f'<{total_samples}h', audio_data[:total_samples*2])
314
+ max_amp = max(abs(s) for s in all_samples)
315
+ avg_amp = sum(abs(s) for s in all_samples) / total_samples
316
+
317
+ # Sessizlik kontrolü
318
+ silent = max_amp < 100 # Linear16 için düşük eşik
319
+
320
+ log_info(f" Audio stats - Max: {max_amp}, Avg: {avg_amp:.1f}, Silent: {silent}")
321
+
322
+ # Eğer çok sessizse uyar
323
+ if max_amp < 50:
324
+ log_warning(f"⚠️ Very low audio level detected! Max amplitude: {max_amp}")
325
+
326
+ self.audio_debug_counters[session_id] += 1
327
+
328
+ except Exception as e:
329
+ log_error(f"Error analyzing audio chunk: {e}")
330
+
331
+ # Audio data from client
332
+ await self.event_bus.publish(Event(
333
+ type=EventType.AUDIO_CHUNK_RECEIVED,
334
+ session_id=session_id,
335
+ data={
336
+ "audio_data": message.get("data"),
337
+ "timestamp": message.get("timestamp")
338
+ }
339
+ ))
340
+
341
+ elif message_type == "control":
342
+ # Control messages
343
+ action = message.get("action")
344
+ config = message.get("config", {})
345
+
346
+ if action == "start_conversation":
347
+ # Yeni action: Mevcut session için conversation başlat
348
+ log_info(f"🎤 Starting conversation for session | session_id={session_id}")
349
+
350
+ await self.event_bus.publish(Event(
351
+ type=EventType.CONVERSATION_STARTED,
352
+ session_id=session_id,
353
+ data={
354
+ "config": config,
355
+ "continuous_listening": config.get("continuous_listening", True)
356
+ }
357
+ ))
358
+
359
+ # Send confirmation to client
360
+ await self.send_message(session_id, {
361
+ "type": "conversation_started",
362
+ "message": "Conversation started successfully"
363
+ })
364
+
365
+ elif action == "stop_conversation":
366
+ await self.event_bus.publish(Event(
367
+ type=EventType.CONVERSATION_ENDED,
368
+ session_id=session_id,
369
+ data={"reason": "user_request"}
370
+ ))
371
+
372
+ elif action == "start_session":
373
+ # Bu artık kullanılmamalı
374
+ log_warning(f"⚠️ Deprecated start_session action received | session_id={session_id}")
375
+
376
+ # Yine de işle ama conversation_started olarak
377
+ await self.event_bus.publish(Event(
378
+ type=EventType.CONVERSATION_STARTED,
379
+ session_id=session_id,
380
+ data=config
381
+ ))
382
+
383
+ elif action == "stop_session":
384
+ await self.event_bus.publish(Event(
385
+ type=EventType.CONVERSATION_ENDED,
386
+ session_id=session_id,
387
+ data={"reason": "user_request"}
388
+ ))
389
+
390
+ elif action == "end_session":
391
+ await self.event_bus.publish(Event(
392
+ type=EventType.SESSION_ENDED,
393
+ session_id=session_id,
394
+ data={"reason": "user_request"}
395
+ ))
396
+
397
+ elif action == "audio_ended":
398
+ await self.event_bus.publish(Event(
399
+ type=EventType.AUDIO_PLAYBACK_COMPLETED,
400
+ session_id=session_id,
401
+ data={}
402
+ ))
403
+
404
+ else:
405
+ log_warning(
406
+ f"⚠️ Unknown control action",
407
+ session_id=session_id,
408
+ action=action
409
+ )
410
+
411
+ elif message_type == "ping":
412
+ # Respond to ping
413
+ await self.send_message(session_id, {"type": "pong"})
414
+
415
+ else:
416
+ log_warning(
417
+ f"⚠️ Unknown message type",
418
+ session_id=session_id,
419
+ message_type=message_type
420
+ )
421
+
422
+ async def send_message(self, session_id: str, message: dict):
423
+ """Queue message for sending to client"""
424
+ queue = self.message_queues.get(session_id)
425
+ if queue:
426
+ await queue.put(message)
427
+ else:
428
+ log_warning(
429
+ f"⚠️ No queue for session",
430
+ session_id=session_id
431
+ )
432
+
433
+ async def broadcast_to_session(self, session_id: str, message: dict):
434
+ """Send message immediately (bypass queue)"""
435
+ connection = self.connections.get(session_id)
436
+ if connection and connection.is_active:
437
+ await connection.send_json(message)
438
+
439
+ # Event handlers for sending messages to clients
440
+
441
+ async def _handle_state_transition(self, event: Event):
442
+ """Send state transition to client"""
443
+ await self.send_message(event.session_id, {
444
+ "type": "state_change",
445
+ "from": event.data.get("old_state"),
446
+ "to": event.data.get("new_state")
447
+ })
448
+
449
+ async def _handle_stt_ready(self, event: Event):
450
+ """Send STT ready signal to client"""
451
+ await self.send_message(event.session_id, {
452
+ "type": "stt_ready",
453
+ "message": "STT is ready to receive audio"
454
+ })
455
+
456
+ async def _handle_stt_stopped(self, event: Event):
457
+ """Send STT stopped signal to client"""
458
+ await self.send_message(event.session_id, {
459
+ "type": "stt_stopped",
460
+ "message": "STT stopped, please stop sending audio"
461
+ })
462
+
463
+ async def _handle_stt_result(self, event: Event):
464
+ """Send STT result to client"""
465
+ # Her türlü result'ı (interim + final) frontend'e gönder
466
+ await self.send_message(event.session_id, {
467
+ "type": "transcription",
468
+ "text": event.data.get("text", ""),
469
+ "is_final": event.data.get("is_final", False),
470
+ "confidence": event.data.get("confidence", 0.0)
471
+ })
472
+
473
+ async def _handle_tts_started(self, event: Event):
474
+ """Send assistant message when TTS starts"""
475
+ if event.data.get("is_welcome"):
476
+ # Send welcome message to client
477
+ await self.send_message(event.session_id, {
478
+ "type": "assistant_response",
479
+ "text": event.data.get("text", ""),
480
+ "is_welcome": True
481
+ })
482
+
483
+ async def _handle_tts_chunk(self, event: Event):
484
+ """Send TTS audio chunk to client"""
485
+ await self.send_message(event.session_id, {
486
+ "type": "tts_audio",
487
+ "data": event.data.get("audio_data"),
488
+ "chunk_index": event.data.get("chunk_index"),
489
+ "total_chunks": event.data.get("total_chunks"),
490
+ "is_last": event.data.get("is_last", False),
491
+ "mime_type": event.data.get("mime_type", "audio/mpeg")
492
+ })
493
+
494
+ async def _handle_tts_completed(self, event: Event):
495
+ """Notify client that TTS is complete"""
496
+ # Client knows from is_last flag in chunks
497
+ pass
498
+
499
+ async def _handle_llm_response(self, event: Event):
500
+ """Send LLM response to client"""
501
+ await self.send_message(event.session_id, {
502
+ "type": "assistant_response",
503
+ "text": event.data.get("text", ""),
504
+ "is_welcome": event.data.get("is_welcome", False)
505
+ })
506
+
507
+ async def _handle_error(self, event: Event):
508
+ """Send error to client"""
509
+ error_type = event.data.get("error_type", "unknown")
510
+ message = event.data.get("message", "An error occurred")
511
+
512
+ await self.send_message(event.session_id, {
513
+ "type": "error",
514
+ "error_type": error_type,
515
+ "message": message,
516
+ "details": event.data.get("details", {})
517
+ })
518
+
519
+ def get_connection_count(self) -> int:
520
+ """Get number of active connections"""
521
+ return len(self.connections)
522
+
523
+ def get_session_connections(self) -> Set[str]:
524
+ """Get all active session IDs"""
525
+ return set(self.connections.keys())
526
+
527
+ async def close_all_connections(self):
528
+ """Close all active connections"""
529
+ session_ids = list(self.connections.keys())
530
+ for session_id in session_ids:
531
  await self.disconnect(session_id)