NitinBot001 commited on
Commit
d58ab7d
·
verified ·
1 Parent(s): d4e1d7d

Upload gemini_tts_api.py

Browse files
Files changed (1) hide show
  1. gemini_tts_api.py +617 -0
gemini_tts_api.py ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gemini TTS API Server
4
+ A FastAPI-based REST API for Google's Gemini Text-to-Speech service
5
+ with concurrent request handling and audio format conversion.
6
+ """
7
+
8
+ import os
9
+ import asyncio
10
+ import json
11
+ import base64
12
+ import uuid
13
+ from datetime import datetime
14
+ from typing import Optional, List, Dict, Any
15
+ from io import BytesIO
16
+ import aiohttp
17
+ import aiofiles
18
+ from fastapi import FastAPI, HTTPException, BackgroundTasks, Request
19
+ from fastapi.responses import FileResponse, JSONResponse
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from pydantic import BaseModel, Field
22
+ from pydub import AudioSegment
23
+ import uvicorn
24
+
25
+ # Pydantic models for request/response
26
+ class VoiceConfig(BaseModel):
27
+ voice_name: str = Field(default="Zephyr", description="Voice name (e.g., Zephyr, Puck)")
28
+
29
+ class SpeakerConfig(BaseModel):
30
+ speaker: str = Field(description="Speaker identifier")
31
+ voice_config: VoiceConfig
32
+
33
+ class TTSRequest(BaseModel):
34
+ text: str = Field(description="Text to convert to speech")
35
+ speakers: Optional[List[SpeakerConfig]] = Field(
36
+ default=None,
37
+ description="Multi-speaker configuration (optional)"
38
+ )
39
+ voice_name: Optional[str] = Field(
40
+ default="Zephyr",
41
+ description="Single voice name (used if speakers not provided)"
42
+ )
43
+ output_format: str = Field(default="wav", description="Output format: wav or mp3")
44
+ speed_factor: float = Field(default=1.0, description="Speed adjustment factor")
45
+ temperature: float = Field(default=1.0, description="Generation temperature")
46
+
47
+ class TTSResponse(BaseModel):
48
+ task_id: str
49
+ status: str
50
+ message: str
51
+ audio_url: Optional[str] = None
52
+ metadata: Optional[Dict[str, Any]] = None
53
+
54
+ class TaskStatus(BaseModel):
55
+ task_id: str
56
+ status: str
57
+ progress: Optional[str] = None
58
+ error: Optional[str] = None
59
+ result: Optional[Dict[str, Any]] = None
60
+
61
+ # Global task storage (in production, use Redis or database)
62
+ tasks: Dict[str, Dict[str, Any]] = {}
63
+
64
+ # FastAPI app initialization
65
+ app = FastAPI(
66
+ title="Gemini TTS API",
67
+ description="Text-to-Speech API using Google's Gemini model with concurrent request handling",
68
+ version="1.0.0"
69
+ )
70
+
71
+ # CORS middleware
72
+ app.add_middleware(
73
+ CORSMiddleware,
74
+ allow_origins=["*"], # Configure appropriately for production
75
+ allow_credentials=True,
76
+ allow_methods=["*"],
77
+ allow_headers=["*"],
78
+ )
79
+
80
+ # Configuration
81
+ def get_api_keys():
82
+ """Get API keys from environment variables"""
83
+ # Support multiple formats for API keys
84
+ api_keys = []
85
+
86
+ # Single API key (backward compatibility)
87
+ single_key = os.getenv('GEMINI_API_KEY')
88
+ if single_key:
89
+ api_keys.append(single_key.strip())
90
+
91
+ # Multiple API keys (comma-separated)
92
+ multi_keys = os.getenv('GEMINI_API_KEYS')
93
+ if multi_keys:
94
+ keys = [key.strip() for key in multi_keys.split(',') if key.strip()]
95
+ api_keys.extend(keys)
96
+
97
+ # Individual API keys (GEMINI_API_KEY_1, GEMINI_API_KEY_2, etc.)
98
+ i = 1
99
+ while True:
100
+ key = os.getenv(f'GEMINI_API_KEY_{i}')
101
+ if not key:
102
+ break
103
+ api_keys.append(key.strip())
104
+ i += 1
105
+
106
+ # Remove duplicates while preserving order
107
+ seen = set()
108
+ unique_keys = []
109
+ for key in api_keys:
110
+ if key not in seen:
111
+ seen.add(key)
112
+ unique_keys.append(key)
113
+
114
+ return unique_keys
115
+
116
+ GEMINI_API_KEYS = get_api_keys()
117
+ MODEL_ID = "gemini-2.5-flash-preview-tts"
118
+ GENERATE_CONTENT_API = "streamGenerateContent"
119
+ OUTPUT_DIR = "audio_files"
120
+ MAX_CONCURRENT_REQUESTS = 10
121
+ RATE_LIMIT_RETRY_DELAY = 60 # seconds to wait after rate limit
122
+ MAX_RETRIES_PER_KEY = 2
123
+
124
+ # API key management
125
+ class APIKeyManager:
126
+ def __init__(self, api_keys: List[str]):
127
+ self.api_keys = api_keys
128
+ self.current_key_index = 0
129
+ self.key_stats = {key: {"requests": 0, "failures": 0, "last_rate_limit": None} for key in api_keys}
130
+ self.lock = asyncio.Lock()
131
+
132
+ async def get_next_key(self) -> Optional[str]:
133
+ """Get the next available API key"""
134
+ async with self.lock:
135
+ if not self.api_keys:
136
+ return None
137
+
138
+ # Try to find a key that's not rate limited
139
+ for _ in range(len(self.api_keys)):
140
+ key = self.api_keys[self.current_key_index]
141
+ stats = self.key_stats[key]
142
+
143
+ # Check if this key is currently rate limited
144
+ if stats["last_rate_limit"]:
145
+ time_since_limit = datetime.now().timestamp() - stats["last_rate_limit"]
146
+ if time_since_limit < RATE_LIMIT_RETRY_DELAY:
147
+ # Still rate limited, try next key
148
+ self.current_key_index = (self.current_key_index + 1) % len(self.api_keys)
149
+ continue
150
+ else:
151
+ # Rate limit period has passed, reset
152
+ stats["last_rate_limit"] = None
153
+
154
+ # This key is available
155
+ stats["requests"] += 1
156
+ return key
157
+
158
+ # All keys are rate limited, return the one with oldest rate limit
159
+ oldest_key = min(
160
+ self.api_keys,
161
+ key=lambda k: self.key_stats[k]["last_rate_limit"] or 0
162
+ )
163
+ return oldest_key
164
+
165
+ async def mark_rate_limited(self, api_key: str):
166
+ """Mark an API key as rate limited"""
167
+ async with self.lock:
168
+ if api_key in self.key_stats:
169
+ self.key_stats[api_key]["last_rate_limit"] = datetime.now().timestamp()
170
+ self.key_stats[api_key]["failures"] += 1
171
+
172
+ async def mark_success(self, api_key: str):
173
+ """Mark an API key as successful (reset failure count)"""
174
+ async with self.lock:
175
+ if api_key in self.key_stats:
176
+ self.key_stats[api_key]["failures"] = max(0, self.key_stats[api_key]["failures"] - 1)
177
+
178
+ def get_stats(self) -> dict:
179
+ """Get statistics for all API keys"""
180
+ return {
181
+ "total_keys": len(self.api_keys),
182
+ "key_stats": self.key_stats.copy()
183
+ }
184
+
185
+ # Initialize API key manager
186
+ api_key_manager = APIKeyManager(GEMINI_API_KEYS)
187
+
188
+ # Ensure output directory exists
189
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
190
+
191
+ # Semaphore to limit concurrent requests
192
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
193
+
194
+ async def convert_and_adjust_audio(
195
+ audio_data: bytes,
196
+ output_format: str = "wav",
197
+ speed_factor: float = 1.0
198
+ ) -> tuple[bytes, str]:
199
+ """
200
+ Convert PCM audio data to specified format and adjust speed asynchronously
201
+ """
202
+ def _convert():
203
+ # Create AudioSegment from raw PCM data
204
+ audio = AudioSegment(
205
+ data=audio_data,
206
+ sample_width=2, # 16-bit = 2 bytes
207
+ frame_rate=24000, # 24kHz
208
+ channels=1 # mono
209
+ )
210
+
211
+ # Adjust speed by changing frame rate
212
+ if speed_factor != 1.0:
213
+ new_frame_rate = int(audio.frame_rate * speed_factor)
214
+ audio_speed_adjusted = audio._spawn(
215
+ audio.raw_data,
216
+ overrides={"frame_rate": new_frame_rate}
217
+ )
218
+ audio_speed_adjusted = audio_speed_adjusted.set_frame_rate(audio.frame_rate)
219
+ else:
220
+ audio_speed_adjusted = audio
221
+
222
+ # Export to desired format
223
+ buffer = BytesIO()
224
+ if output_format.lower() == "mp3":
225
+ audio_speed_adjusted.export(buffer, format="mp3", bitrate="128k")
226
+ return buffer.getvalue(), "mp3"
227
+ else:
228
+ audio_speed_adjusted.export(buffer, format="wav")
229
+ return buffer.getvalue(), "wav"
230
+
231
+ # Run audio processing in thread pool to avoid blocking
232
+ loop = asyncio.get_event_loop()
233
+ return await loop.run_in_executor(None, _convert)
234
+
235
+ async def generate_tts_audio(
236
+ task_id: str,
237
+ text: str,
238
+ speakers: Optional[List[SpeakerConfig]] = None,
239
+ voice_name: str = "Zephyr",
240
+ output_format: str = "wav",
241
+ speed_factor: float = 1.0,
242
+ temperature: float = 1.0
243
+ ):
244
+ """
245
+ Generate TTS audio using Gemini API with multiple API key support and rate limit handling
246
+ """
247
+ async with semaphore: # Limit concurrent requests
248
+ try:
249
+ # Update task status
250
+ tasks[task_id]["status"] = "processing"
251
+ tasks[task_id]["progress"] = "Preparing request"
252
+
253
+ # Prepare request data
254
+ if speakers:
255
+ # Multi-speaker configuration
256
+ speech_config = {
257
+ "multi_speaker_voice_config": {
258
+ "speaker_voice_configs": [
259
+ {
260
+ "speaker": speaker.speaker,
261
+ "voice_config": {
262
+ "prebuilt_voice_config": {
263
+ "voice_name": speaker.voice_config.voice_name
264
+ }
265
+ }
266
+ }
267
+ for speaker in speakers
268
+ ]
269
+ }
270
+ }
271
+ else:
272
+ # Single voice configuration
273
+ speech_config = {
274
+ "voice_config": {
275
+ "prebuilt_voice_config": {
276
+ "voice_name": voice_name
277
+ }
278
+ }
279
+ }
280
+
281
+ request_data = {
282
+ "contents": [
283
+ {
284
+ "role": "user",
285
+ "parts": [{"text": text}]
286
+ }
287
+ ],
288
+ "generationConfig": {
289
+ "responseModalities": ["audio"],
290
+ "temperature": temperature,
291
+ "speech_config": speech_config
292
+ }
293
+ }
294
+
295
+ # API endpoint
296
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL_ID}:{GENERATE_CONTENT_API}"
297
+
298
+ tasks[task_id]["progress"] = "Calling Gemini API"
299
+
300
+ # Try multiple API keys with rate limit handling
301
+ last_error = None
302
+ attempts = 0
303
+ max_total_attempts = len(GEMINI_API_KEYS) * MAX_RETRIES_PER_KEY if GEMINI_API_KEYS else 1
304
+
305
+ while attempts < max_total_attempts:
306
+ current_api_key = await api_key_manager.get_next_key()
307
+ if not current_api_key:
308
+ raise HTTPException(status_code=500, detail="No API keys available")
309
+
310
+ attempts += 1
311
+ tasks[task_id]["progress"] = f"Attempting API call (attempt {attempts}/{max_total_attempts})"
312
+
313
+ try:
314
+ # Make async API request
315
+ async with aiohttp.ClientSession() as session:
316
+ async with session.post(
317
+ url,
318
+ headers={"Content-Type": "application/json"},
319
+ params={"key": current_api_key},
320
+ json=request_data,
321
+ timeout=aiohttp.ClientTimeout(total=120) # 2 minute timeout
322
+ ) as response:
323
+
324
+ # Handle different HTTP status codes
325
+ if response.status == 200:
326
+ # Success! Mark key as successful and proceed
327
+ await api_key_manager.mark_success(current_api_key)
328
+ response_data = await response.json()
329
+ break
330
+
331
+ elif response.status == 429: # Rate limit exceeded
332
+ error_text = await response.text()
333
+ await api_key_manager.mark_rate_limited(current_api_key)
334
+ last_error = f"Rate limit exceeded for API key: {error_text}"
335
+ print(f"Rate limit hit for key ending in ...{current_api_key[-4:]}, trying next key")
336
+ continue
337
+
338
+ elif response.status in [403, 401]: # Auth errors
339
+ error_text = await response.text()
340
+ await api_key_manager.mark_rate_limited(current_api_key) # Temporarily disable this key
341
+ last_error = f"Authentication error: {error_text}"
342
+ print(f"Auth error for key ending in ...{current_api_key[-4:]}: {error_text}")
343
+ continue
344
+
345
+ else: # Other HTTP errors
346
+ error_text = await response.text()
347
+ last_error = f"HTTP {response.status}: {error_text}"
348
+ # Don't mark as rate limited for other errors, but still try next key
349
+ continue
350
+
351
+ except asyncio.TimeoutError:
352
+ last_error = "Request timeout"
353
+ continue
354
+ except aiohttp.ClientError as e:
355
+ last_error = f"Client error: {str(e)}"
356
+ continue
357
+ except Exception as e:
358
+ last_error = f"Unexpected error: {str(e)}"
359
+ continue
360
+
361
+ else:
362
+ # All attempts failed
363
+ raise HTTPException(
364
+ status_code=500,
365
+ detail=f"All API keys exhausted. Last error: {last_error}"
366
+ )
367
+
368
+ tasks[task_id]["progress"] = "Processing audio data"
369
+
370
+ # Extract audio data
371
+ if response_data and len(response_data) > 0:
372
+ candidates = response_data[0].get("candidates", [])
373
+ if not candidates:
374
+ raise HTTPException(status_code=500, detail="No candidates in response")
375
+
376
+ parts = candidates[0].get("content", {}).get("parts", [])
377
+ audio_data_b64 = None
378
+
379
+ for part in parts:
380
+ if "inlineData" in part:
381
+ audio_data_b64 = part["inlineData"].get("data", "")
382
+ break
383
+
384
+ if not audio_data_b64:
385
+ raise HTTPException(status_code=500, detail="No audio data found in response")
386
+
387
+ # Decode base64 audio data
388
+ audio_data = base64.b64decode(audio_data_b64)
389
+
390
+ tasks[task_id]["progress"] = "Converting audio format"
391
+
392
+ # Convert and adjust audio
393
+ converted_audio, file_ext = await convert_and_adjust_audio(
394
+ audio_data, output_format, speed_factor
395
+ )
396
+
397
+ # Generate filename
398
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
399
+ filename = f"gemini_audio_{task_id}_{timestamp}.{file_ext}"
400
+ filepath = os.path.join(OUTPUT_DIR, filename)
401
+
402
+ # Save audio file
403
+ async with aiofiles.open(filepath, "wb") as f:
404
+ await f.write(converted_audio)
405
+
406
+ # Update task with results
407
+ tasks[task_id].update({
408
+ "status": "completed",
409
+ "progress": "Completed",
410
+ "result": {
411
+ "filename": filename,
412
+ "filepath": filepath,
413
+ "format": output_format.upper(),
414
+ "speed_factor": speed_factor,
415
+ "original_size": len(audio_data),
416
+ "converted_size": len(converted_audio),
417
+ "audio_url": f"/audio/{filename}"
418
+ }
419
+ })
420
+
421
+ except Exception as e:
422
+ tasks[task_id].update({
423
+ "status": "failed",
424
+ "error": str(e)
425
+ })
426
+
427
+ # API Endpoints
428
+
429
+ @app.get("/")
430
+ async def root():
431
+ """Root endpoint with API information"""
432
+ return {
433
+ "message": "Gemini TTS API Server",
434
+ "version": "1.0.0",
435
+ "endpoints": {
436
+ "POST /tts": "Generate TTS audio",
437
+ "GET /status/{task_id}": "Get task status",
438
+ "GET /audio/{filename}": "Download audio file",
439
+ "GET /tasks": "List all tasks"
440
+ }
441
+ }
442
+
443
+ @app.post("/tts", response_model=TTSResponse)
444
+ async def create_tts_task(
445
+ request: TTSRequest,
446
+ background_tasks: BackgroundTasks
447
+ ):
448
+ """
449
+ Create a new TTS generation task
450
+ """
451
+ if not GEMINI_API_KEYS:
452
+ raise HTTPException(status_code=500, detail="No GEMINI_API_KEYs configured. Please set GEMINI_API_KEY, GEMINI_API_KEYS, or GEMINI_API_KEY_1, GEMINI_API_KEY_2, etc.")
453
+
454
+ # Generate unique task ID
455
+ task_id = str(uuid.uuid4())
456
+
457
+ # Initialize task
458
+ tasks[task_id] = {
459
+ "task_id": task_id,
460
+ "status": "queued",
461
+ "created_at": datetime.now().isoformat(),
462
+ "request": request.dict()
463
+ }
464
+
465
+ # Start background task
466
+ background_tasks.add_task(
467
+ generate_tts_audio,
468
+ task_id,
469
+ request.text,
470
+ request.speakers,
471
+ request.voice_name,
472
+ request.output_format,
473
+ request.speed_factor,
474
+ request.temperature
475
+ )
476
+
477
+ return TTSResponse(
478
+ task_id=task_id,
479
+ status="queued",
480
+ message="TTS generation task created successfully"
481
+ )
482
+
483
+ @app.get("/status/{task_id}", response_model=TaskStatus)
484
+ async def get_task_status(task_id: str):
485
+ """
486
+ Get the status of a TTS generation task
487
+ """
488
+ if task_id not in tasks:
489
+ raise HTTPException(status_code=404, detail="Task not found")
490
+
491
+ task = tasks[task_id]
492
+ return TaskStatus(
493
+ task_id=task_id,
494
+ status=task["status"],
495
+ progress=task.get("progress"),
496
+ error=task.get("error"),
497
+ result=task.get("result")
498
+ )
499
+
500
+ @app.get("/audio/{filename}")
501
+ async def download_audio(filename: str):
502
+ """
503
+ Download generated audio file
504
+ """
505
+ filepath = os.path.join(OUTPUT_DIR, filename)
506
+ if not os.path.exists(filepath):
507
+ raise HTTPException(status_code=404, detail="Audio file not found")
508
+
509
+ return FileResponse(
510
+ filepath,
511
+ media_type="application/octet-stream",
512
+ filename=filename
513
+ )
514
+
515
+ @app.get("/tasks")
516
+ async def list_tasks():
517
+ """
518
+ List all tasks with their current status
519
+ """
520
+ return {"tasks": list(tasks.values())}
521
+
522
+ @app.delete("/tasks/{task_id}")
523
+ async def delete_task(task_id: str):
524
+ """
525
+ Delete a task and its associated audio file
526
+ """
527
+ if task_id not in tasks:
528
+ raise HTTPException(status_code=404, detail="Task not found")
529
+
530
+ task = tasks[task_id]
531
+
532
+ # Delete audio file if it exists
533
+ if task.get("result") and task["result"].get("filepath"):
534
+ filepath = task["result"]["filepath"]
535
+ if os.path.exists(filepath):
536
+ os.remove(filepath)
537
+
538
+ # Remove task from memory
539
+ del tasks[task_id]
540
+
541
+ return {"message": "Task deleted successfully"}
542
+
543
+ @app.get("/health")
544
+ async def health_check():
545
+ """
546
+ Health check endpoint with API key status
547
+ """
548
+ api_stats = api_key_manager.get_stats()
549
+
550
+ return {
551
+ "status": "healthy",
552
+ "timestamp": datetime.now().isoformat(),
553
+ "active_tasks": len([t for t in tasks.values() if t["status"] in ["queued", "processing"]]),
554
+ "total_tasks": len(tasks),
555
+ "api_keys": {
556
+ "total_configured": api_stats["total_keys"],
557
+ "available_keys": len([
558
+ key for key, stats in api_stats["key_stats"].items()
559
+ if not stats["last_rate_limit"] or
560
+ (datetime.now().timestamp() - stats["last_rate_limit"]) > RATE_LIMIT_RETRY_DELAY
561
+ ]),
562
+ "rate_limited_keys": len([
563
+ key for key, stats in api_stats["key_stats"].items()
564
+ if stats["last_rate_limit"] and
565
+ (datetime.now().timestamp() - stats["last_rate_limit"]) <= RATE_LIMIT_RETRY_DELAY
566
+ ])
567
+ }
568
+ }
569
+
570
+ @app.get("/api-keys/stats")
571
+ async def get_api_key_stats():
572
+ """
573
+ Get detailed statistics for all API keys
574
+ """
575
+ stats = api_key_manager.get_stats()
576
+
577
+ # Mask API keys for security (show only last 4 characters)
578
+ masked_stats = {}
579
+ for key, data in stats["key_stats"].items():
580
+ masked_key = f"***{key[-4:]}" if len(key) > 4 else "***"
581
+ masked_stats[masked_key] = {
582
+ **data,
583
+ "is_rate_limited": (
584
+ data["last_rate_limit"] and
585
+ (datetime.now().timestamp() - data["last_rate_limit"]) <= RATE_LIMIT_RETRY_DELAY
586
+ ) if data["last_rate_limit"] else False,
587
+ "time_until_available": max(0, RATE_LIMIT_RETRY_DELAY - (
588
+ datetime.now().timestamp() - data["last_rate_limit"]
589
+ )) if data["last_rate_limit"] else 0
590
+ }
591
+
592
+ return {
593
+ "total_keys": stats["total_keys"],
594
+ "key_statistics": masked_stats,
595
+ "rate_limit_settings": {
596
+ "retry_delay_seconds": RATE_LIMIT_RETRY_DELAY,
597
+ "max_retries_per_key": MAX_RETRIES_PER_KEY
598
+ }
599
+ }
600
+
601
+ # Cleanup old files periodically (you might want to implement this with a proper scheduler)
602
+ async def cleanup_old_files():
603
+ """
604
+ Clean up old audio files and completed tasks
605
+ """
606
+ # This is a simple implementation - consider using APScheduler for production
607
+ pass
608
+
609
+ if __name__ == "__main__":
610
+ # Configuration for running the server
611
+ uvicorn.run(
612
+ "gemini_tts_api:app",
613
+ host="0.0.0.0",
614
+ port=8000,
615
+ reload=True, # Set to False in production
616
+ workers=1 # Use multiple workers in production with proper task storage
617
+ )