| | |
| |
|
| | import os |
| | import asyncio |
| | import uuid |
| | import wave |
| | import re |
| | from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request |
| | from fastapi.responses import HTMLResponse |
| | from fastapi.staticfiles import StaticFiles |
| | from fastapi.templating import Jinja2Templates |
| | from google import genai |
| | from google.genai import types |
| |
|
| | |
| | class ApiKeyManager: |
| | def __init__(self, api_keys_str: str): |
| | if not api_keys_str: |
| | raise ValueError("متغیر ALL_GEMINI_API_KEYS پیدا نشد یا خالی است!") |
| | self.keys = [key.strip() for key in api_keys_str.split(',') if key.strip()] |
| | if not self.keys: |
| | raise ValueError("هیچ کلید معتبری در متغیر ALL_GEMINI_API_KEYS یافت نشد.") |
| | print(f"تعداد {len(self.keys)} کلید API با موفقیت بارگذاری شد.") |
| | self._index = 0 |
| | self._lock = asyncio.Lock() |
| |
|
| | async def get_next_key(self) -> tuple[int, str]: |
| | async with self._lock: |
| | key_index = self._index |
| | api_key = self.keys[key_index] |
| | self._index = (self._index + 1) % len(self.keys) |
| | return key_index, api_key |
| |
|
| | ALL_API_KEYS = os.environ.get("ALL_GEMINI_API_KEYS") |
| | api_key_manager = ApiKeyManager(ALL_API_KEYS) |
| |
|
| | |
| | MODEL = "models/gemini-2.5-flash-native-audio-preview-09-2025" |
| | CONFIG = types.LiveConnectConfig( |
| | response_modalities=["AUDIO"], |
| | speech_config=types.SpeechConfig( |
| | voice_config=types.VoiceConfig( |
| | prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Zephyr") |
| | ) |
| | ), |
| | ) |
| |
|
| | app = FastAPI() |
| |
|
| | TEMP_DIR = "/tmp/temp_audio" |
| | os.makedirs(TEMP_DIR, exist_ok=True) |
| | app.mount("/audio", StaticFiles(directory=TEMP_DIR), name="audio") |
| |
|
| | templates = Jinja2Templates(directory="templates") |
| |
|
| | |
| | SAMPLE_RATE = 24000 |
| | CHANNELS = 1 |
| | SAMPLE_WIDTH = 2 |
| | AUDIO_BUFFER_SIZE = 8192 |
| |
|
| | def split_text_into_chunks(text: str, max_chunk_size: int = 800): |
| | if len(text) <= max_chunk_size: |
| | return [text] |
| | sentences = re.split(r'(?<=[.!?؟])\s+', text.strip()) |
| | chunks, current_chunk = [], "" |
| | for sentence in sentences: |
| | if not sentence: continue |
| | if len(current_chunk) + len(sentence) + 1 > max_chunk_size and current_chunk: |
| | chunks.append(current_chunk.strip()) |
| | current_chunk = sentence |
| | else: |
| | current_chunk += (" " + sentence) if current_chunk else sentence |
| | if current_chunk: chunks.append(current_chunk.strip()) |
| | return chunks |
| |
|
| | async def process_and_stream_one_chunk(websocket: WebSocket, chunk: str, request_id: str, chunk_num: int, total_chunks: int): |
| | """ |
| | این تابع مسئولیت پردازش یک قطعه متن را بر عهده دارد. |
| | آنقدر با کلیدهای مختلف تلاش میکند تا بالاخره موفق شود. |
| | اگر با تمام کلیدها ناموفق بود، یک استثنا (Exception) ایجاد میکند. |
| | """ |
| | max_retries = len(api_key_manager.keys) |
| | log_prefix = f"[{request_id}][قطعه {chunk_num}/{total_chunks}]" |
| | |
| | for attempt in range(max_retries): |
| | key_index, api_key = await api_key_manager.get_next_key() |
| | |
| | |
| | unique_id_for_chunk = str(uuid.uuid4())[:8] |
| | tts_prompt = f"Please ignore the ID in brackets and just read the text: '{chunk}' [ID: {unique_id_for_chunk}]" |
| |
|
| | print(f"{log_prefix} تلاش {attempt + 1}/{max_retries} با کلید {key_index}...") |
| |
|
| | try: |
| | client = genai.Client(http_options={"api_version": "v1beta"}, api_key=api_key) |
| | audio_buffer = bytearray() |
| | full_chunk_audio = [] |
| | |
| | async with client.aio.live.connect(model=MODEL, config=CONFIG) as session: |
| | await session.send(input=tts_prompt, end_of_turn=True) |
| | turn = session.receive() |
| | |
| | |
| | first_response = await asyncio.wait_for(anext(turn), timeout=10.0) |
| | |
| | print(f"{log_prefix} ارتباط موفق با کلید {key_index}. شروع استریم...") |
| | |
| | if data := first_response.data: |
| | full_chunk_audio.append(data) |
| | audio_buffer.extend(data) |
| |
|
| | async for response in turn: |
| | if data := response.data: |
| | full_chunk_audio.append(data) |
| | audio_buffer.extend(data) |
| | if len(audio_buffer) >= AUDIO_BUFFER_SIZE: |
| | await websocket.send_bytes(audio_buffer) |
| | audio_buffer = bytearray() |
| | |
| | if audio_buffer: |
| | await websocket.send_bytes(audio_buffer) |
| | |
| | |
| | return b"".join(full_chunk_audio) |
| |
|
| | except Exception as e: |
| | print(f"{log_prefix} خطا با کلید {key_index}: {e}. تلاش با کلید بعدی...") |
| | |
| | |
| | |
| | raise Exception(f"پردازش {log_prefix} با تمام {max_retries} کلید API ناموفق بود.") |
| |
|
| |
|
| | @app.get("/", response_class=HTMLResponse) |
| | async def read_root(request: Request): |
| | return templates.TemplateResponse("index.html", {"request": request}) |
| |
|
| | @app.websocket("/ws") |
| | async def websocket_endpoint(websocket: WebSocket): |
| | await websocket.accept() |
| | client_id = str(uuid.uuid4())[:6] |
| | print(f"کلاینت جدید [{client_id}] متصل شد.") |
| |
|
| | try: |
| | while True: |
| | text_prompt_from_user = await websocket.receive_text() |
| | request_id = f"{client_id}-{str(uuid.uuid4())[:6]}" |
| | print(f"[{request_id}] درخواست جدید دریافت شد.") |
| |
|
| | try: |
| | text_chunks = split_text_into_chunks(text_prompt_from_user) |
| | full_audio_for_file = [] |
| |
|
| | |
| | for i, chunk in enumerate(text_chunks): |
| | |
| | chunk_audio = await process_and_stream_one_chunk( |
| | websocket, chunk, request_id, i + 1, len(text_chunks) |
| | ) |
| | full_audio_for_file.append(chunk_audio) |
| | |
| | |
| | filename = f"{request_id}.wav" |
| | filepath = os.path.join(TEMP_DIR, filename) |
| | with wave.open(filepath, 'wb') as wf: |
| | wf.setnchannels(CHANNELS); wf.setsampwidth(SAMPLE_WIDTH) |
| | wf.setframerate(SAMPLE_RATE); wf.writeframes(b"".join(full_audio_for_file)) |
| | print(f"[{request_id}] فایل صوتی کامل در {filepath} ذخیره شد.") |
| | await websocket.send_json({"event": "STREAM_ENDED", "url": f"/audio/{filename}"}) |
| |
|
| | print(f"[{request_id}] استریم با موفقیت به پایان رسید.") |
| |
|
| | except Exception as e: |
| | |
| | error_message = f"خطای بحرانی: {e}" |
| | print(f"[{request_id}] {error_message}") |
| | await websocket.send_json({"event": "ERROR", "message": str(e)}) |
| |
|
| | except WebSocketDisconnect: |
| | print(f"کلاینت [{client_id}] اتصال را قطع کرد.") |
| | except Exception as e: |
| | print(f"[{client_id}] یک خطای پیشبینینشده در WebSocket رخ داد: {e}") |
| | finally: |
| | print(f"ارتباط با کلاینت [{client_id}] بسته شد.") |
| |
|
| | |