ciyidogan commited on
Commit
1784117
·
verified ·
1 Parent(s): 95e2fc0

Delete tts_lifecycle_manager.py

Browse files
Files changed (1) hide show
  1. tts_lifecycle_manager.py +0 -377
tts_lifecycle_manager.py DELETED
@@ -1,377 +0,0 @@
1
- """
2
- TTS Lifecycle Manager for Flare
3
- ===============================
4
- Manages TTS instances lifecycle per session
5
- """
6
- import asyncio
7
- from typing import Dict, Optional, Any, List
8
- from datetime import datetime
9
- import traceback
10
- import base64
11
-
12
- from event_bus import EventBus, Event, EventType, publish_error
13
- from resource_manager import ResourceManager, ResourceType
14
- from tts.tts_factory import TTSFactory
15
- from tts.tts_interface import TTSInterface
16
- from tts.tts_preprocessor import TTSPreprocessor
17
- from utils.logger import log_info, log_error, log_debug, log_warning
18
-
19
-
20
- class TTSJob:
21
- """TTS synthesis job"""
22
-
23
- def __init__(self, job_id: str, session_id: str, text: str, is_welcome: bool = False):
24
- self.job_id = job_id
25
- self.session_id = session_id
26
- self.text = text
27
- self.is_welcome = is_welcome
28
- self.created_at = datetime.utcnow()
29
- self.completed_at: Optional[datetime] = None
30
- self.audio_data: Optional[bytes] = None
31
- self.error: Optional[str] = None
32
- self.chunks_sent = 0
33
-
34
- def complete(self, audio_data: bytes):
35
- """Mark job as completed"""
36
- self.audio_data = audio_data
37
- self.completed_at = datetime.utcnow()
38
-
39
- def fail(self, error: str):
40
- """Mark job as failed"""
41
- self.error = error
42
- self.completed_at = datetime.utcnow()
43
-
44
-
45
- class TTSSession:
46
- """TTS session wrapper"""
47
-
48
- def __init__(self, session_id: str, tts_instance: TTSInterface):
49
- self.session_id = session_id
50
- self.tts_instance = tts_instance
51
- self.preprocessor: Optional[TTSPreprocessor] = None
52
- self.active_jobs: Dict[str, TTSJob] = {}
53
- self.completed_jobs: List[TTSJob] = []
54
- self.created_at = datetime.utcnow()
55
- self.last_activity = datetime.utcnow()
56
- self.total_jobs = 0
57
- self.total_chars = 0
58
-
59
- def update_activity(self):
60
- """Update last activity timestamp"""
61
- self.last_activity = datetime.utcnow()
62
-
63
-
64
- class TTSLifecycleManager:
65
- """Manages TTS instances lifecycle"""
66
-
67
- def __init__(self, event_bus: EventBus, resource_manager: ResourceManager):
68
- self.event_bus = event_bus
69
- self.resource_manager = resource_manager
70
- self.tts_sessions: Dict[str, TTSSession] = {}
71
- self.chunk_size = 16384 # 16KB chunks for base64
72
- self._setup_event_handlers()
73
- self._setup_resource_pool()
74
-
75
- def _setup_event_handlers(self):
76
- """Subscribe to TTS-related events"""
77
- self.event_bus.subscribe(EventType.TTS_STARTED, self._handle_tts_start)
78
- self.event_bus.subscribe(EventType.SESSION_ENDED, self._handle_session_ended)
79
-
80
- def _setup_resource_pool(self):
81
- """Setup TTS instance pool"""
82
- self.resource_manager.register_pool(
83
- resource_type=ResourceType.TTS_INSTANCE,
84
- factory=self._create_tts_instance,
85
- max_idle=3,
86
- max_age_seconds=600 # 10 minutes
87
- )
88
-
89
- async def _create_tts_instance(self) -> Optional[TTSInterface]:
90
- """Factory for creating TTS instances"""
91
- try:
92
- tts_instance = TTSFactory.create_provider()
93
- if not tts_instance:
94
- log_warning("⚠️ No TTS provider configured")
95
- return None
96
-
97
- log_debug("🔊 Created new TTS instance")
98
- return tts_instance
99
-
100
- except Exception as e:
101
- log_error(f"❌ Failed to create TTS instance", error=str(e))
102
- return None
103
-
104
- async def _handle_tts_start(self, event: Event):
105
- """Handle TTS synthesis request"""
106
- session_id = event.session_id
107
- text = event.data.get("text", "")
108
- is_welcome = event.data.get("is_welcome", False)
109
-
110
- if not text:
111
- log_warning(f"⚠️ Empty text for TTS", session_id=session_id)
112
- return
113
-
114
- try:
115
- log_info(
116
- f"🔊 Starting TTS",
117
- session_id=session_id,
118
- text_length=len(text),
119
- is_welcome=is_welcome
120
- )
121
-
122
- # Get or create session
123
- if session_id not in self.tts_sessions:
124
- # Acquire TTS instance from pool
125
- resource_id = f"tts_{session_id}"
126
- tts_instance = await self.resource_manager.acquire(
127
- resource_id=resource_id,
128
- session_id=session_id,
129
- resource_type=ResourceType.TTS_INSTANCE,
130
- cleanup_callback=self._cleanup_tts_instance
131
- )
132
-
133
- if not tts_instance:
134
- # No TTS available
135
- await self._handle_no_tts(session_id, text, is_welcome)
136
- return
137
-
138
- # Create session
139
- tts_session = TTSSession(session_id, tts_instance)
140
-
141
- # Get locale from event data or default
142
- locale = event.data.get("locale", "tr")
143
- tts_session.preprocessor = TTSPreprocessor(language=locale)
144
-
145
- self.tts_sessions[session_id] = tts_session
146
- else:
147
- tts_session = self.tts_sessions[session_id]
148
-
149
- # Create job
150
- job_id = f"{session_id}_{tts_session.total_jobs}"
151
- job = TTSJob(job_id, session_id, text, is_welcome)
152
- tts_session.active_jobs[job_id] = job
153
- tts_session.total_jobs += 1
154
- tts_session.total_chars += len(text)
155
- tts_session.update_activity()
156
-
157
- # Process TTS
158
- await self._process_tts_job(tts_session, job)
159
-
160
- except Exception as e:
161
- log_error(
162
- f"❌ Failed to start TTS",
163
- session_id=session_id,
164
- error=str(e),
165
- traceback=traceback.format_exc()
166
- )
167
-
168
- # Publish error event
169
- await publish_error(
170
- session_id=session_id,
171
- error_type="tts_error",
172
- error_message=f"Failed to synthesize speech: {str(e)}"
173
- )
174
-
175
- async def _process_tts_job(self, tts_session: TTSSession, job: TTSJob):
176
- """Process a TTS job"""
177
- try:
178
- # Preprocess text
179
- processed_text = tts_session.preprocessor.preprocess(
180
- job.text,
181
- tts_session.tts_instance.get_preprocessing_flags()
182
- )
183
-
184
- log_debug(
185
- f"📝 TTS preprocessed",
186
- session_id=job.session_id,
187
- original_length=len(job.text),
188
- processed_length=len(processed_text)
189
- )
190
-
191
- # Synthesize audio
192
- audio_data = await tts_session.tts_instance.synthesize(processed_text)
193
-
194
- if not audio_data:
195
- raise ValueError("TTS returned empty audio data")
196
-
197
- job.complete(audio_data)
198
-
199
- log_info(
200
- f"✅ TTS synthesis complete",
201
- session_id=job.session_id,
202
- audio_size=len(audio_data),
203
- duration_ms=(datetime.utcnow() - job.created_at).total_seconds() * 1000
204
- )
205
-
206
- # Stream audio chunks
207
- await self._stream_audio_chunks(tts_session, job)
208
-
209
- # Move to completed
210
- tts_session.active_jobs.pop(job.job_id, None)
211
- tts_session.completed_jobs.append(job)
212
-
213
- # Keep only last 10 completed jobs
214
- if len(tts_session.completed_jobs) > 10:
215
- tts_session.completed_jobs.pop(0)
216
-
217
- except Exception as e:
218
- job.fail(str(e))
219
-
220
- # Handle specific TTS errors
221
- error_message = str(e)
222
- if "quota" in error_message.lower() or "limit" in error_message.lower():
223
- log_error(f"❌ TTS quota exceeded", session_id=job.session_id)
224
- await publish_error(
225
- session_id=job.session_id,
226
- error_type="tts_quota_exceeded",
227
- error_message="TTS service quota exceeded"
228
- )
229
- else:
230
- log_error(
231
- f"❌ TTS synthesis failed",
232
- session_id=job.session_id,
233
- error=error_message
234
- )
235
- await publish_error(
236
- session_id=job.session_id,
237
- error_type="tts_error",
238
- error_message=error_message
239
- )
240
-
241
- async def _stream_audio_chunks(self, tts_session: TTSSession, job: TTSJob):
242
- """Stream audio data as chunks"""
243
- if not job.audio_data:
244
- return
245
-
246
- # Convert to base64
247
- audio_base64 = base64.b64encode(job.audio_data).decode('utf-8')
248
- total_length = len(audio_base64)
249
- total_chunks = (total_length + self.chunk_size - 1) // self.chunk_size
250
-
251
- log_debug(
252
- f"📤 Streaming TTS audio",
253
- session_id=job.session_id,
254
- total_size=len(job.audio_data),
255
- base64_size=total_length,
256
- chunks=total_chunks
257
- )
258
-
259
- # Stream chunks
260
- for i in range(0, total_length, self.chunk_size):
261
- chunk = audio_base64[i:i + self.chunk_size]
262
- chunk_index = i // self.chunk_size
263
- is_last = chunk_index == total_chunks - 1
264
-
265
- await self.event_bus.publish(Event(
266
- type=EventType.TTS_CHUNK_READY,
267
- session_id=job.session_id,
268
- data={
269
- "audio_data": chunk,
270
- "chunk_index": chunk_index,
271
- "total_chunks": total_chunks,
272
- "is_last": is_last,
273
- "mime_type": "audio/mpeg",
274
- "is_welcome": job.is_welcome
275
- },
276
- priority=8 # Higher priority for audio chunks
277
- ))
278
-
279
- job.chunks_sent += 1
280
-
281
- # Small delay between chunks to prevent overwhelming
282
- await asyncio.sleep(0.01)
283
-
284
- # Notify completion
285
- await self.event_bus.publish(Event(
286
- type=EventType.TTS_COMPLETED,
287
- session_id=job.session_id,
288
- data={
289
- "job_id": job.job_id,
290
- "total_chunks": total_chunks,
291
- "is_welcome": job.is_welcome
292
- }
293
- ))
294
-
295
- log_info(
296
- f"✅ TTS streaming complete",
297
- session_id=job.session_id,
298
- chunks_sent=job.chunks_sent
299
- )
300
-
301
- async def _handle_no_tts(self, session_id: str, text: str, is_welcome: bool):
302
- """Handle case when TTS is not available"""
303
- log_warning(f"⚠️ No TTS available, skipping audio generation", session_id=session_id)
304
-
305
- # Just notify completion without audio
306
- await self.event_bus.publish(Event(
307
- type=EventType.TTS_COMPLETED,
308
- session_id=session_id,
309
- data={
310
- "no_audio": True,
311
- "text": text,
312
- "is_welcome": is_welcome
313
- }
314
- ))
315
-
316
- async def _handle_session_ended(self, event: Event):
317
- """Clean up TTS resources when session ends"""
318
- session_id = event.session_id
319
- await self._cleanup_session(session_id)
320
-
321
- async def _cleanup_session(self, session_id: str):
322
- """Clean up TTS session"""
323
- tts_session = self.tts_sessions.pop(session_id, None)
324
- if not tts_session:
325
- return
326
-
327
- try:
328
- # Cancel any active jobs
329
- for job in tts_session.active_jobs.values():
330
- if not job.completed_at:
331
- job.fail("Session ended")
332
-
333
- # Release resource
334
- resource_id = f"tts_{session_id}"
335
- await self.resource_manager.release(resource_id, delay_seconds=120)
336
-
337
- log_info(
338
- f"🧹 TTS session cleaned up",
339
- session_id=session_id,
340
- total_jobs=tts_session.total_jobs,
341
- total_chars=tts_session.total_chars
342
- )
343
-
344
- except Exception as e:
345
- log_error(
346
- f"❌ Error cleaning up TTS session",
347
- session_id=session_id,
348
- error=str(e)
349
- )
350
-
351
- async def _cleanup_tts_instance(self, tts_instance: TTSInterface):
352
- """Cleanup callback for TTS instance"""
353
- try:
354
- # TTS instances typically don't need special cleanup
355
- log_debug("🧹 TTS instance cleaned up")
356
-
357
- except Exception as e:
358
- log_error(f"❌ Error cleaning up TTS instance", error=str(e))
359
-
360
- def get_stats(self) -> Dict[str, Any]:
361
- """Get TTS manager statistics"""
362
- session_stats = {}
363
- for session_id, tts_session in self.tts_sessions.items():
364
- session_stats[session_id] = {
365
- "active_jobs": len(tts_session.active_jobs),
366
- "completed_jobs": len(tts_session.completed_jobs),
367
- "total_jobs": tts_session.total_jobs,
368
- "total_chars": tts_session.total_chars,
369
- "uptime_seconds": (datetime.utcnow() - tts_session.created_at).total_seconds(),
370
- "last_activity": tts_session.last_activity.isoformat()
371
- }
372
-
373
- return {
374
- "active_sessions": len(self.tts_sessions),
375
- "total_active_jobs": sum(len(s.active_jobs) for s in self.tts_sessions.values()),
376
- "sessions": session_stats
377
- }