ciyidogan commited on
Commit
52fad77
Β·
verified Β·
1 Parent(s): 367b121

Update stt/stt_lifecycle_manager.py

Browse files
Files changed (1) hide show
  1. stt/stt_lifecycle_manager.py +373 -367
stt/stt_lifecycle_manager.py CHANGED
@@ -1,368 +1,374 @@
1
- """
2
- STT Lifecycle Manager for Flare
3
- ===============================
4
- Manages STT instances lifecycle per session
5
- """
6
- import asyncio
7
- from typing import Dict, Optional, Any
8
- from datetime import datetime
9
- import traceback
10
- import base64
11
-
12
- from chat_session.event_bus import EventBus, Event, EventType, publish_error
13
- from chat_session.resource_manager import ResourceManager, ResourceType
14
- from stt.stt_factory import STTFactory
15
- from stt.stt_interface import STTInterface, STTConfig, TranscriptionResult
16
- from utils.logger import log_info, log_error, log_debug, log_warning
17
-
18
-
19
- class STTSession:
20
- """STT session wrapper"""
21
-
22
- def __init__(self, session_id: str, stt_instance: STTInterface):
23
- self.session_id = session_id
24
- self.stt_instance = stt_instance
25
- self.is_streaming = False
26
- self.config: Optional[STTConfig] = None
27
- self.created_at = datetime.utcnow()
28
- self.last_activity = datetime.utcnow()
29
- self.total_chunks = 0
30
- self.total_bytes = 0
31
-
32
- def update_activity(self):
33
- """Update last activity timestamp"""
34
- self.last_activity = datetime.utcnow()
35
-
36
-
37
- class STTLifecycleManager:
38
- """Manages STT instances lifecycle"""
39
-
40
- def __init__(self, event_bus: EventBus, resource_manager: ResourceManager):
41
- self.event_bus = event_bus
42
- self.resource_manager = resource_manager
43
- self.stt_sessions: Dict[str, STTSession] = {}
44
- self._setup_event_handlers()
45
- self._setup_resource_pool()
46
-
47
- def _setup_event_handlers(self):
48
- """Subscribe to STT-related events"""
49
- self.event_bus.subscribe(EventType.STT_STARTED, self._handle_stt_start)
50
- self.event_bus.subscribe(EventType.STT_STOPPED, self._handle_stt_stop)
51
- self.event_bus.subscribe(EventType.AUDIO_CHUNK_RECEIVED, self._handle_audio_chunk)
52
- self.event_bus.subscribe(EventType.SESSION_ENDED, self._handle_session_ended)
53
-
54
- def _setup_resource_pool(self):
55
- """Setup STT instance pool"""
56
- self.resource_manager.register_pool(
57
- resource_type=ResourceType.STT_INSTANCE,
58
- factory=self._create_stt_instance,
59
- max_idle=5,
60
- max_age_seconds=300 # 5 minutes
61
- )
62
-
63
- async def _create_stt_instance(self) -> STTInterface:
64
- """Factory for creating STT instances"""
65
- try:
66
- stt_instance = STTFactory.create_provider()
67
- if not stt_instance:
68
- raise ValueError("Failed to create STT instance")
69
-
70
- log_debug("🎀 Created new STT instance")
71
- return stt_instance
72
-
73
- except Exception as e:
74
- log_error(f"❌ Failed to create STT instance", error=str(e))
75
- raise
76
-
77
- async def _handle_stt_start(self, event: Event):
78
- """Handle STT start request"""
79
- session_id = event.session_id
80
- config_data = event.data
81
-
82
- try:
83
- log_info(f"🎀 Starting STT", session_id=session_id)
84
-
85
- # Check if already exists
86
- if session_id in self.stt_sessions:
87
- stt_session = self.stt_sessions[session_id]
88
- if stt_session.is_streaming:
89
- log_warning(f"⚠️ STT already streaming", session_id=session_id)
90
- return
91
- else:
92
- # Acquire STT instance from pool
93
- resource_id = f"stt_{session_id}"
94
- stt_instance = await self.resource_manager.acquire(
95
- resource_id=resource_id,
96
- session_id=session_id,
97
- resource_type=ResourceType.STT_INSTANCE,
98
- cleanup_callback=self._cleanup_stt_instance
99
- )
100
-
101
- # Create session wrapper
102
- stt_session = STTSession(session_id, stt_instance)
103
- self.stt_sessions[session_id] = stt_session
104
-
105
- # Get session locale from state orchestrator
106
- locale = config_data.get("locale", "tr")
107
-
108
- # Build STT config - βœ… CONTINUOUS LISTENING Δ°Γ‡Δ°N AYARLAR
109
- stt_config = STTConfig(
110
- language=self._get_language_code(locale),
111
- sample_rate=config_data.get("sample_rate", 16000),
112
- encoding=config_data.get("encoding", "WEBM_OPUS"), # Try "LINEAR16" if WEBM fails
113
- enable_punctuation=config_data.get("enable_punctuation", True),
114
- enable_word_timestamps=False,
115
- model=config_data.get("model", "latest_long"),
116
- use_enhanced=config_data.get("use_enhanced", True),
117
- single_utterance=False, # βœ… Continuous listening iΓ§in FALSE olmalΔ±
118
- interim_results=True, # βœ… Interim results'Δ± AΓ‡
119
- )
120
-
121
- # Log the exact config being used
122
- log_info(f"πŸ“‹ STT Config: encoding={stt_config.encoding}, "
123
- f"sample_rate={stt_config.sample_rate}, "
124
- f"single_utterance={stt_config.single_utterance}, "
125
- f"interim_results={stt_config.interim_results}")
126
-
127
- stt_session.config = stt_config
128
-
129
- # Start streaming
130
- await stt_session.stt_instance.start_streaming(stt_config)
131
- stt_session.is_streaming = True
132
- stt_session.update_activity()
133
-
134
- log_info(f"βœ… STT started in continuous mode with interim results", session_id=session_id, language=stt_config.language)
135
-
136
- # Notify STT is ready
137
- await self.event_bus.publish(Event(
138
- type=EventType.STT_READY,
139
- session_id=session_id,
140
- data={"language": stt_config.language}
141
- ))
142
-
143
- except Exception as e:
144
- log_error(
145
- f"❌ Failed to start STT",
146
- session_id=session_id,
147
- error=str(e),
148
- traceback=traceback.format_exc()
149
- )
150
-
151
- # Clean up on error
152
- if session_id in self.stt_sessions:
153
- await self._cleanup_session(session_id)
154
-
155
- # Publish error event
156
- await publish_error(
157
- session_id=session_id,
158
- error_type="stt_error",
159
- error_message=f"Failed to start STT: {str(e)}"
160
- )
161
-
162
- async def _handle_stt_stop(self, event: Event):
163
- """Handle STT stop request"""
164
- session_id = event.session_id
165
- reason = event.data.get("reason", "unknown")
166
-
167
- log_info(f"πŸ›‘ Stopping STT", session_id=session_id, reason=reason)
168
-
169
- stt_session = self.stt_sessions.get(session_id)
170
- if not stt_session:
171
- log_warning(f"⚠️ No STT session found", session_id=session_id)
172
- return
173
-
174
- try:
175
- if stt_session.is_streaming:
176
- # Stop streaming
177
- final_result = await stt_session.stt_instance.stop_streaming()
178
- stt_session.is_streaming = False
179
-
180
- # If we got a final result, publish it
181
- if final_result and final_result.text:
182
- await self.event_bus.publish(Event(
183
- type=EventType.STT_RESULT,
184
- session_id=session_id,
185
- data={
186
- "text": final_result.text,
187
- "is_final": True,
188
- "confidence": final_result.confidence
189
- }
190
- ))
191
-
192
- # Don't remove session immediately - might restart
193
- stt_session.update_activity()
194
-
195
- log_info(f"βœ… STT stopped", session_id=session_id)
196
-
197
- except Exception as e:
198
- log_error(
199
- f"❌ Error stopping STT",
200
- session_id=session_id,
201
- error=str(e)
202
- )
203
-
204
- async def _handle_audio_chunk(self, event: Event):
205
- """Process audio chunk through STT"""
206
- session_id = event.session_id
207
-
208
- stt_session = self.stt_sessions.get(session_id)
209
- if not stt_session or not stt_session.is_streaming:
210
- # STT not ready, ignore chunk
211
- return
212
-
213
- try:
214
- # Decode audio data
215
- audio_data = base64.b64decode(event.data.get("audio_data", ""))
216
-
217
- # Update stats
218
- stt_session.total_chunks += 1
219
- stt_session.total_bytes += len(audio_data)
220
- stt_session.update_activity()
221
-
222
- # Stream to STT
223
- async for result in stt_session.stt_instance.stream_audio(audio_data):
224
- # Publish transcription results
225
- await self.event_bus.publish(Event(
226
- type=EventType.STT_RESULT,
227
- session_id=session_id,
228
- data={
229
- "text": result.text,
230
- "is_final": result.is_final,
231
- "confidence": result.confidence,
232
- "timestamp": result.timestamp
233
- }
234
- ))
235
-
236
- # Log final results
237
- if result.is_final:
238
- log_info(
239
- f"πŸ“ STT final result",
240
- session_id=session_id,
241
- text=result.text[:50] + "..." if len(result.text) > 50 else result.text,
242
- confidence=result.confidence
243
- )
244
-
245
- # Log progress periodically
246
- if stt_session.total_chunks % 100 == 0:
247
- log_debug(
248
- f"πŸ“Š STT progress",
249
- session_id=session_id,
250
- chunks=stt_session.total_chunks,
251
- bytes=stt_session.total_bytes
252
- )
253
-
254
- except Exception as e:
255
- log_error(
256
- f"❌ Error processing audio chunk",
257
- session_id=session_id,
258
- error=str(e)
259
- )
260
-
261
- # Check if it's a recoverable error
262
- if "stream duration" in str(e) or "timeout" in str(e).lower():
263
- # STT timeout, restart needed
264
- await publish_error(
265
- session_id=session_id,
266
- error_type="stt_timeout",
267
- error_message="STT stream timeout, restart needed"
268
- )
269
- else:
270
- # Other STT error
271
- await publish_error(
272
- session_id=session_id,
273
- error_type="stt_error",
274
- error_message=str(e)
275
- )
276
-
277
- async def _handle_session_ended(self, event: Event):
278
- """Clean up STT resources when session ends"""
279
- session_id = event.session_id
280
- await self._cleanup_session(session_id)
281
-
282
- async def _cleanup_session(self, session_id: str):
283
- """Clean up STT session"""
284
- stt_session = self.stt_sessions.pop(session_id, None)
285
- if not stt_session:
286
- return
287
-
288
- try:
289
- # Stop streaming if active
290
- if stt_session.is_streaming:
291
- await stt_session.stt_instance.stop_streaming()
292
-
293
- # Release resource
294
- resource_id = f"stt_{session_id}"
295
- await self.resource_manager.release(resource_id, delay_seconds=60)
296
-
297
- log_info(
298
- f"🧹 STT session cleaned up",
299
- session_id=session_id,
300
- total_chunks=stt_session.total_chunks,
301
- total_bytes=stt_session.total_bytes
302
- )
303
-
304
- except Exception as e:
305
- log_error(
306
- f"❌ Error cleaning up STT session",
307
- session_id=session_id,
308
- error=str(e)
309
- )
310
-
311
- async def _cleanup_stt_instance(self, stt_instance: STTInterface):
312
- """Cleanup callback for STT instance"""
313
- try:
314
- # Ensure streaming is stopped
315
- if hasattr(stt_instance, 'is_streaming') and stt_instance.is_streaming:
316
- await stt_instance.stop_streaming()
317
-
318
- log_debug("🧹 STT instance cleaned up")
319
-
320
- except Exception as e:
321
- log_error(f"❌ Error cleaning up STT instance", error=str(e))
322
-
323
- def _get_language_code(self, locale: str) -> str:
324
- """Convert locale to STT language code"""
325
- # Map common locales to STT language codes
326
- locale_map = {
327
- "tr": "tr-TR",
328
- "en": "en-US",
329
- "de": "de-DE",
330
- "fr": "fr-FR",
331
- "es": "es-ES",
332
- "it": "it-IT",
333
- "pt": "pt-BR",
334
- "ru": "ru-RU",
335
- "ja": "ja-JP",
336
- "ko": "ko-KR",
337
- "zh": "zh-CN",
338
- "ar": "ar-SA"
339
- }
340
-
341
- # Check direct match
342
- if locale in locale_map:
343
- return locale_map[locale]
344
-
345
- # Check if it's already a full code
346
- if "-" in locale and len(locale) == 5:
347
- return locale
348
-
349
- # Default to locale-LOCALE format
350
- return f"{locale}-{locale.upper()}"
351
-
352
- def get_stats(self) -> Dict[str, Any]:
353
- """Get STT manager statistics"""
354
- session_stats = {}
355
- for session_id, stt_session in self.stt_sessions.items():
356
- session_stats[session_id] = {
357
- "is_streaming": stt_session.is_streaming,
358
- "total_chunks": stt_session.total_chunks,
359
- "total_bytes": stt_session.total_bytes,
360
- "uptime_seconds": (datetime.utcnow() - stt_session.created_at).total_seconds(),
361
- "last_activity": stt_session.last_activity.isoformat()
362
- }
363
-
364
- return {
365
- "active_sessions": len(self.stt_sessions),
366
- "streaming_sessions": sum(1 for s in self.stt_sessions.values() if s.is_streaming),
367
- "sessions": session_stats
 
 
 
 
 
 
368
  }
 
1
+ """
2
+ STT Lifecycle Manager for Flare
3
+ ===============================
4
+ Manages STT instances lifecycle per session
5
+ """
6
+ import asyncio
7
+ from typing import Dict, Optional, Any
8
+ from datetime import datetime
9
+ import traceback
10
+ import base64
11
+
12
+ from chat_session.event_bus import EventBus, Event, EventType, publish_error
13
+ from chat_session.resource_manager import ResourceManager, ResourceType
14
+ from stt.stt_factory import STTFactory
15
+ from stt.stt_interface import STTInterface, STTConfig, TranscriptionResult
16
+ from utils.logger import log_info, log_error, log_debug, log_warning
17
+
18
+
19
+ class STTSession:
20
+ """STT session wrapper"""
21
+
22
+ def __init__(self, session_id: str, stt_instance: STTInterface):
23
+ self.session_id = session_id
24
+ self.stt_instance = stt_instance
25
+ self.is_streaming = False
26
+ self.config: Optional[STTConfig] = None
27
+ self.created_at = datetime.utcnow()
28
+ self.last_activity = datetime.utcnow()
29
+ self.total_chunks = 0
30
+ self.total_bytes = 0
31
+
32
+ def update_activity(self):
33
+ """Update last activity timestamp"""
34
+ self.last_activity = datetime.utcnow()
35
+
36
+
37
+ class STTLifecycleManager:
38
+ """Manages STT instances lifecycle"""
39
+
40
+ def __init__(self, event_bus: EventBus, resource_manager: ResourceManager):
41
+ self.event_bus = event_bus
42
+ self.resource_manager = resource_manager
43
+ self.stt_sessions: Dict[str, STTSession] = {}
44
+ self._setup_event_handlers()
45
+ self._setup_resource_pool()
46
+
47
+ def _setup_event_handlers(self):
48
+ """Subscribe to STT-related events"""
49
+ self.event_bus.subscribe(EventType.STT_STARTED, self._handle_stt_start)
50
+ self.event_bus.subscribe(EventType.STT_STOPPED, self._handle_stt_stop)
51
+ self.event_bus.subscribe(EventType.AUDIO_CHUNK_RECEIVED, self._handle_audio_chunk)
52
+ self.event_bus.subscribe(EventType.SESSION_ENDED, self._handle_session_ended)
53
+
54
+ def _setup_resource_pool(self):
55
+ """Setup STT instance pool"""
56
+ self.resource_manager.register_pool(
57
+ resource_type=ResourceType.STT_INSTANCE,
58
+ factory=self._create_stt_instance,
59
+ max_idle=5,
60
+ max_age_seconds=300 # 5 minutes
61
+ )
62
+
63
+ async def _create_stt_instance(self) -> STTInterface:
64
+ """Factory for creating STT instances"""
65
+ try:
66
+ stt_instance = STTFactory.create_provider()
67
+ if not stt_instance:
68
+ raise ValueError("Failed to create STT instance")
69
+
70
+ log_debug("🎀 Created new STT instance")
71
+ return stt_instance
72
+
73
+ except Exception as e:
74
+ log_error(f"❌ Failed to create STT instance", error=str(e))
75
+ raise
76
+
77
+ async def _handle_stt_start(self, event: Event):
78
+ """Handle STT start request"""
79
+ session_id = event.session_id
80
+ config_data = event.data
81
+
82
+ try:
83
+ log_info(f"🎀 Starting STT", session_id=session_id)
84
+
85
+ # Check if already exists
86
+ if session_id in self.stt_sessions:
87
+ stt_session = self.stt_sessions[session_id]
88
+ if stt_session.is_streaming:
89
+ log_warning(f"⚠️ STT already streaming", session_id=session_id)
90
+ return
91
+ else:
92
+ # Acquire STT instance from pool
93
+ resource_id = f"stt_{session_id}"
94
+ stt_instance = await self.resource_manager.acquire(
95
+ resource_id=resource_id,
96
+ session_id=session_id,
97
+ resource_type=ResourceType.STT_INSTANCE,
98
+ cleanup_callback=self._cleanup_stt_instance
99
+ )
100
+
101
+ # Create session wrapper
102
+ stt_session = STTSession(session_id, stt_instance)
103
+ self.stt_sessions[session_id] = stt_session
104
+
105
+ # Get session locale from state orchestrator
106
+ locale = config_data.get("locale", "tr")
107
+
108
+ # Build STT config - βœ… CONTINUOUS LISTENING Δ°Γ‡Δ°N AYARLAR
109
+ stt_config = STTConfig(
110
+ language=self._get_language_code(locale),
111
+ sample_rate=config_data.get("sample_rate", 16000),
112
+ encoding=config_data.get("encoding", "WEBM_OPUS"), # Try "LINEAR16" if WEBM fails
113
+ enable_punctuation=config_data.get("enable_punctuation", True),
114
+ enable_word_timestamps=False,
115
+ model=config_data.get("model", "latest_long"),
116
+ use_enhanced=config_data.get("use_enhanced", True),
117
+ single_utterance=False, # βœ… Continuous listening iΓ§in FALSE olmalΔ±
118
+ interim_results=True, # βœ… Interim results'Δ± AΓ‡
119
+ )
120
+
121
+ # Log the exact config being used
122
+ log_info(f"πŸ“‹ STT Config: encoding={stt_config.encoding}, "
123
+ f"sample_rate={stt_config.sample_rate}, "
124
+ f"single_utterance={stt_config.single_utterance}, "
125
+ f"interim_results={stt_config.interim_results}")
126
+
127
+ stt_session.config = stt_config
128
+
129
+ # Start streaming
130
+ await stt_session.stt_instance.start_streaming(stt_config)
131
+ stt_session.is_streaming = True
132
+ stt_session.update_activity()
133
+
134
+ log_info(f"βœ… STT started in continuous mode with interim results", session_id=session_id, language=stt_config.language)
135
+
136
+ # Notify STT is ready
137
+ await self.event_bus.publish(Event(
138
+ type=EventType.STT_READY,
139
+ session_id=session_id,
140
+ data={"language": stt_config.language}
141
+ ))
142
+
143
+ except Exception as e:
144
+ log_error(
145
+ f"❌ Failed to start STT",
146
+ session_id=session_id,
147
+ error=str(e),
148
+ traceback=traceback.format_exc()
149
+ )
150
+
151
+ # Clean up on error
152
+ if session_id in self.stt_sessions:
153
+ await self._cleanup_session(session_id)
154
+
155
+ # Publish error event
156
+ await publish_error(
157
+ session_id=session_id,
158
+ error_type="stt_error",
159
+ error_message=f"Failed to start STT: {str(e)}"
160
+ )
161
+
162
+ async def _handle_stt_stop(self, event: Event):
163
+ """Handle STT stop request"""
164
+ session_id = event.session_id
165
+ reason = event.data.get("reason", "unknown")
166
+
167
+ """Send STT stopped signal to client"""
168
+ await self.send_message(session_id, {
169
+ "type": "stt_stopped",
170
+ "message": "STT stopped, please stop sending audio"
171
+ })
172
+
173
+ log_info(f"πŸ›‘ Stopping STT", session_id=session_id, reason=reason)
174
+
175
+ stt_session = self.stt_sessions.get(session_id)
176
+ if not stt_session:
177
+ log_warning(f"⚠️ No STT session found", session_id=session_id)
178
+ return
179
+
180
+ try:
181
+ if stt_session.is_streaming:
182
+ # Stop streaming
183
+ final_result = await stt_session.stt_instance.stop_streaming()
184
+ stt_session.is_streaming = False
185
+
186
+ # If we got a final result, publish it
187
+ if final_result and final_result.text:
188
+ await self.event_bus.publish(Event(
189
+ type=EventType.STT_RESULT,
190
+ session_id=session_id,
191
+ data={
192
+ "text": final_result.text,
193
+ "is_final": True,
194
+ "confidence": final_result.confidence
195
+ }
196
+ ))
197
+
198
+ # Don't remove session immediately - might restart
199
+ stt_session.update_activity()
200
+
201
+ log_info(f"βœ… STT stopped", session_id=session_id)
202
+
203
+ except Exception as e:
204
+ log_error(
205
+ f"❌ Error stopping STT",
206
+ session_id=session_id,
207
+ error=str(e)
208
+ )
209
+
210
+ async def _handle_audio_chunk(self, event: Event):
211
+ """Process audio chunk through STT"""
212
+ session_id = event.session_id
213
+
214
+ stt_session = self.stt_sessions.get(session_id)
215
+ if not stt_session or not stt_session.is_streaming:
216
+ # STT not ready, ignore chunk
217
+ return
218
+
219
+ try:
220
+ # Decode audio data
221
+ audio_data = base64.b64decode(event.data.get("audio_data", ""))
222
+
223
+ # Update stats
224
+ stt_session.total_chunks += 1
225
+ stt_session.total_bytes += len(audio_data)
226
+ stt_session.update_activity()
227
+
228
+ # Stream to STT
229
+ async for result in stt_session.stt_instance.stream_audio(audio_data):
230
+ # Publish transcription results
231
+ await self.event_bus.publish(Event(
232
+ type=EventType.STT_RESULT,
233
+ session_id=session_id,
234
+ data={
235
+ "text": result.text,
236
+ "is_final": result.is_final,
237
+ "confidence": result.confidence,
238
+ "timestamp": result.timestamp
239
+ }
240
+ ))
241
+
242
+ # Log final results
243
+ if result.is_final:
244
+ log_info(
245
+ f"πŸ“ STT final result",
246
+ session_id=session_id,
247
+ text=result.text[:50] + "..." if len(result.text) > 50 else result.text,
248
+ confidence=result.confidence
249
+ )
250
+
251
+ # Log progress periodically
252
+ if stt_session.total_chunks % 100 == 0:
253
+ log_debug(
254
+ f"πŸ“Š STT progress",
255
+ session_id=session_id,
256
+ chunks=stt_session.total_chunks,
257
+ bytes=stt_session.total_bytes
258
+ )
259
+
260
+ except Exception as e:
261
+ log_error(
262
+ f"❌ Error processing audio chunk",
263
+ session_id=session_id,
264
+ error=str(e)
265
+ )
266
+
267
+ # Check if it's a recoverable error
268
+ if "stream duration" in str(e) or "timeout" in str(e).lower():
269
+ # STT timeout, restart needed
270
+ await publish_error(
271
+ session_id=session_id,
272
+ error_type="stt_timeout",
273
+ error_message="STT stream timeout, restart needed"
274
+ )
275
+ else:
276
+ # Other STT error
277
+ await publish_error(
278
+ session_id=session_id,
279
+ error_type="stt_error",
280
+ error_message=str(e)
281
+ )
282
+
283
+ async def _handle_session_ended(self, event: Event):
284
+ """Clean up STT resources when session ends"""
285
+ session_id = event.session_id
286
+ await self._cleanup_session(session_id)
287
+
288
+ async def _cleanup_session(self, session_id: str):
289
+ """Clean up STT session"""
290
+ stt_session = self.stt_sessions.pop(session_id, None)
291
+ if not stt_session:
292
+ return
293
+
294
+ try:
295
+ # Stop streaming if active
296
+ if stt_session.is_streaming:
297
+ await stt_session.stt_instance.stop_streaming()
298
+
299
+ # Release resource
300
+ resource_id = f"stt_{session_id}"
301
+ await self.resource_manager.release(resource_id, delay_seconds=60)
302
+
303
+ log_info(
304
+ f"🧹 STT session cleaned up",
305
+ session_id=session_id,
306
+ total_chunks=stt_session.total_chunks,
307
+ total_bytes=stt_session.total_bytes
308
+ )
309
+
310
+ except Exception as e:
311
+ log_error(
312
+ f"❌ Error cleaning up STT session",
313
+ session_id=session_id,
314
+ error=str(e)
315
+ )
316
+
317
+ async def _cleanup_stt_instance(self, stt_instance: STTInterface):
318
+ """Cleanup callback for STT instance"""
319
+ try:
320
+ # Ensure streaming is stopped
321
+ if hasattr(stt_instance, 'is_streaming') and stt_instance.is_streaming:
322
+ await stt_instance.stop_streaming()
323
+
324
+ log_debug("🧹 STT instance cleaned up")
325
+
326
+ except Exception as e:
327
+ log_error(f"❌ Error cleaning up STT instance", error=str(e))
328
+
329
+ def _get_language_code(self, locale: str) -> str:
330
+ """Convert locale to STT language code"""
331
+ # Map common locales to STT language codes
332
+ locale_map = {
333
+ "tr": "tr-TR",
334
+ "en": "en-US",
335
+ "de": "de-DE",
336
+ "fr": "fr-FR",
337
+ "es": "es-ES",
338
+ "it": "it-IT",
339
+ "pt": "pt-BR",
340
+ "ru": "ru-RU",
341
+ "ja": "ja-JP",
342
+ "ko": "ko-KR",
343
+ "zh": "zh-CN",
344
+ "ar": "ar-SA"
345
+ }
346
+
347
+ # Check direct match
348
+ if locale in locale_map:
349
+ return locale_map[locale]
350
+
351
+ # Check if it's already a full code
352
+ if "-" in locale and len(locale) == 5:
353
+ return locale
354
+
355
+ # Default to locale-LOCALE format
356
+ return f"{locale}-{locale.upper()}"
357
+
358
+ def get_stats(self) -> Dict[str, Any]:
359
+ """Get STT manager statistics"""
360
+ session_stats = {}
361
+ for session_id, stt_session in self.stt_sessions.items():
362
+ session_stats[session_id] = {
363
+ "is_streaming": stt_session.is_streaming,
364
+ "total_chunks": stt_session.total_chunks,
365
+ "total_bytes": stt_session.total_bytes,
366
+ "uptime_seconds": (datetime.utcnow() - stt_session.created_at).total_seconds(),
367
+ "last_activity": stt_session.last_activity.isoformat()
368
+ }
369
+
370
+ return {
371
+ "active_sessions": len(self.stt_sessions),
372
+ "streaming_sessions": sum(1 for s in self.stt_sessions.values() if s.is_streaming),
373
+ "sessions": session_stats
374
  }