Spaces:
Running
Running
from fastapi import FastAPI, HTTPException | |
from fastapi.responses import JSONResponse, StreamingResponse | |
from google import genai | |
from google.genai import types | |
import wave | |
import io | |
import os | |
from typing import Optional, List | |
from pydantic import BaseModel | |
from dotenv import load_dotenv | |
# Load environment variables | |
load_dotenv() | |
app = FastAPI( | |
title="Google GenAI TTS API with Multi-Key Quota Fallback", | |
description="TTS API using Google GenAI with multiple API keys and 429 quota handling.", | |
version="1.3.0", | |
docs_url="/docs", | |
redoc_url=None | |
) | |
class TTSRequest(BaseModel): | |
text: str | |
voice_name: Optional[str] = "Kore" | |
cheerful: Optional[bool] = True | |
sample_rate: Optional[int] = 24000 | |
channels: Optional[int] = 1 | |
sample_width: Optional[int] = 2 | |
def get_api_keys() -> List[str]: | |
api_keys = os.getenv("GEMINI_API_KEYS") | |
if not api_keys: | |
raise ValueError("No API keys found in GEMINI_API_KEYS environment variable.") | |
return [key.strip() for key in api_keys.split(",") if key.strip()] | |
def generate_wave_bytes(pcm_data: bytes, channels: int, rate: int, sample_width: int) -> bytes: | |
with io.BytesIO() as wav_buffer: | |
with wave.open(wav_buffer, "wb") as wf: | |
wf.setnchannels(channels) | |
wf.setsampwidth(sample_width) | |
wf.setframerate(rate) | |
wf.writeframes(pcm_data) | |
return wav_buffer.getvalue() | |
def try_generate_tts(api_key, request): | |
client = genai.Client(api_key=api_key) | |
text_to_speak = f"Say cheerfully: {request.text}" if request.cheerful else request.text | |
response = client.models.generate_content( | |
model="gemini-2.5-flash-preview-tts", | |
contents=text_to_speak, | |
config=types.GenerateContentConfig( | |
response_modalities=["AUDIO"], | |
speech_config=types.SpeechConfig( | |
voice_config=types.VoiceConfig( | |
prebuilt_voice_config=types.PrebuiltVoiceConfig( | |
voice_name=request.voice_name, | |
) | |
) | |
), | |
) | |
) | |
if not response.candidates or not response.candidates[0].content.parts: | |
raise HTTPException(status_code=500, detail="No audio data received from GenAI.") | |
audio_data = response.candidates[0].content.parts[0].inline_data.data | |
wav_bytes = generate_wave_bytes( | |
audio_data, | |
channels=request.channels, | |
rate=request.sample_rate, | |
sample_width=request.sample_width | |
) | |
return wav_bytes | |
async def generate_tts(request: TTSRequest): | |
api_keys = get_api_keys() | |
last_error = None | |
for api_key in api_keys: | |
try: | |
print(f"Trying API key: {api_key[:5]}...") | |
wav_bytes = try_generate_tts(api_key, request) | |
return StreamingResponse( | |
io.BytesIO(wav_bytes), | |
media_type="audio/wav", | |
headers={"Content-Disposition": "attachment; filename=generated_audio.wav"} | |
) | |
except Exception as e: | |
error_msg = str(e) | |
print(f"Key {api_key[:5]} failed: {error_msg}") | |
if "RESOURCE_EXHAUSTED" in error_msg or "quota" in error_msg.lower() or "429" in error_msg: | |
# Quota exhausted, try next key | |
last_error = error_msg | |
continue | |
else: | |
# Some other error, don't continue | |
return JSONResponse( | |
{"status": "error", "message": error_msg}, | |
status_code=500 | |
) | |
return JSONResponse( | |
{"status": "error", "message": f"All API keys exhausted or invalid. Last error: {last_error}"}, | |
status_code=429 | |
) | |
async def root(): | |
return {"message": "Google GenAI TTS API is running"} | |
async def health_check(): | |
return {"status": "healthy"} | |
if __name__ == "__main__": | |
import uvicorn | |
uvicorn.run(app, host="0.0.0.0", port=8080) | |