Opera8 commited on
Commit
57e1f10
·
verified ·
1 Parent(s): 60fc8e4

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -0
app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- START OF FILE app.py ---
2
+
3
+ import os
4
+ import asyncio
5
+ import uuid
6
+ import wave
7
+ import re
8
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
9
+ from fastapi.responses import HTMLResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.templating import Jinja2Templates
12
+ from google import genai
13
+ from google.genai import types
14
+
15
+ # --- مدیریت کلیدهای API ---
16
+ class ApiKeyManager:
17
+ def __init__(self, api_keys_str: str):
18
+ if not api_keys_str:
19
+ raise ValueError("متغیر ALL_GEMINI_API_KEYS پیدا نشد یا خالی است!")
20
+ self.keys = [key.strip() for key in api_keys_str.split(',') if key.strip()]
21
+ if not self.keys:
22
+ raise ValueError("هیچ کلید معتبری در متغیر ALL_GEMINI_API_KEYS یافت نشد.")
23
+ print(f"تعداد {len(self.keys)} کلید API با موفقیت بارگذاری شد.")
24
+ self._index = 0
25
+ self._lock = asyncio.Lock()
26
+
27
+ async def get_next_key(self) -> tuple[int, str]:
28
+ async with self._lock:
29
+ key_index = self._index
30
+ api_key = self.keys[key_index]
31
+ self._index = (self._index + 1) % len(self.keys)
32
+ return key_index, api_key
33
+
34
+ ALL_API_KEYS = os.environ.get("ALL_GEMINI_API_KEYS")
35
+ api_key_manager = ApiKeyManager(ALL_API_KEYS)
36
+
37
+ # --- تنظیمات عمومی Gemini ---
38
+ MODEL = "models/gemini-2.5-flash-native-audio-preview-09-2025"
39
+ CONFIG = types.LiveConnectConfig(
40
+ response_modalities=["AUDIO"],
41
+ speech_config=types.SpeechConfig(
42
+ voice_config=types.VoiceConfig(
43
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Zephyr")
44
+ )
45
+ ),
46
+ )
47
+
48
+ app = FastAPI()
49
+
50
+ TEMP_DIR = "/tmp/temp_audio"
51
+ os.makedirs(TEMP_DIR, exist_ok=True)
52
+ app.mount("/audio", StaticFiles(directory=TEMP_DIR), name="audio")
53
+
54
+ templates = Jinja2Templates(directory="templates")
55
+
56
+ # --- تنظیمات فایل صوتی ---
57
+ SAMPLE_RATE = 24000
58
+ CHANNELS = 1
59
+ SAMPLE_WIDTH = 2
60
+ AUDIO_BUFFER_SIZE = 8192
61
+
62
+ def split_text_into_chunks(text: str, max_chunk_size: int = 800):
63
+ if len(text) <= max_chunk_size:
64
+ return [text]
65
+ sentences = re.split(r'(?<=[.!?؟])\s+', text.strip())
66
+ chunks, current_chunk = [], ""
67
+ for sentence in sentences:
68
+ if not sentence: continue
69
+ if len(current_chunk) + len(sentence) + 1 > max_chunk_size and current_chunk:
70
+ chunks.append(current_chunk.strip())
71
+ current_chunk = sentence
72
+ else:
73
+ current_chunk += (" " + sentence) if current_chunk else sentence
74
+ if current_chunk: chunks.append(current_chunk.strip())
75
+ return chunks
76
+
77
+ async def process_and_stream_one_chunk(websocket: WebSocket, chunk: str, request_id: str, chunk_num: int, total_chunks: int):
78
+ """
79
+ این تابع مسئولیت پردازش یک قطعه متن را بر عهده دارد.
80
+ آنقدر با کلیدهای مختلف تلاش می‌کند تا بالاخره موفق شود.
81
+ اگر با تمام کلیدها ناموفق بود، یک استثنا (Exception) ایجاد می‌کند.
82
+ """
83
+ max_retries = len(api_key_manager.keys)
84
+ log_prefix = f"[{request_id}][قطعه {chunk_num}/{total_chunks}]"
85
+
86
+ for attempt in range(max_retries):
87
+ key_index, api_key = await api_key_manager.get_next_key()
88
+
89
+ # شناسه منحصر به فرد برای جلوگیری از کش شدن در سمت سرور
90
+ unique_id_for_chunk = str(uuid.uuid4())[:8]
91
+ tts_prompt = f"Please ignore the ID in brackets and just read the text: '{chunk}' [ID: {unique_id_for_chunk}]"
92
+
93
+ print(f"{log_prefix} تلاش {attempt + 1}/{max_retries} با کلید {key_index}...")
94
+
95
+ try:
96
+ client = genai.Client(http_options={"api_version": "v1beta"}, api_key=api_key)
97
+ audio_buffer = bytearray()
98
+ full_chunk_audio = []
99
+
100
+ async with client.aio.live.connect(model=MODEL, config=CONFIG) as session:
101
+ await session.send(input=tts_prompt, end_of_turn=True)
102
+ turn = session.receive()
103
+
104
+ # *** تغییر کلیدی: افزایش Timeout به ۱۰ ثانیه طبق درخواست ***
105
+ first_response = await asyncio.wait_for(anext(turn), timeout=10.0)
106
+
107
+ print(f"{log_prefix} ارتباط موفق با کلید {key_index}. شروع استریم...")
108
+
109
+ if data := first_response.data:
110
+ full_chunk_audio.append(data)
111
+ audio_buffer.extend(data)
112
+
113
+ async for response in turn:
114
+ if data := response.data:
115
+ full_chunk_audio.append(data)
116
+ audio_buffer.extend(data)
117
+ if len(audio_buffer) >= AUDIO_BUFFER_SIZE:
118
+ await websocket.send_bytes(audio_buffer)
119
+ audio_buffer = bytearray()
120
+
121
+ if audio_buffer:
122
+ await websocket.send_bytes(audio_buffer)
123
+
124
+ # اگر پردازش موفق بود، داده صوتی را برگردان تا ذخیره شود و از تابع خارج شو
125
+ return b"".join(full_chunk_audio)
126
+
127
+ except Exception as e:
128
+ print(f"{log_prefix} خطا با کلید {key_index}: {e}. تلاش با کلید بعدی...")
129
+ # اگر این آخرین کلید بود و باز هم خطا داد، در دور بعدی حلقه خطا ایجاد خواهد شد
130
+
131
+ # اگر حلقه تمام شد و هیچ کلیدی موفق نبود، یک خطای نهایی ایجاد کن
132
+ raise Exception(f"پردازش {log_prefix} با تمام {max_retries} کلید API ناموفق بود.")
133
+
134
+
135
+ @app.get("/", response_class=HTMLResponse)
136
+ async def read_root(request: Request):
137
+ return templates.TemplateResponse("index.html", {"request": request})
138
+
139
+ @app.websocket("/ws")
140
+ async def websocket_endpoint(websocket: WebSocket):
141
+ await websocket.accept()
142
+ client_id = str(uuid.uuid4())[:6]
143
+ print(f"کلاینت جدید [{client_id}] متصل شد.")
144
+
145
+ try:
146
+ while True:
147
+ text_prompt_from_user = await websocket.receive_text()
148
+ request_id = f"{client_id}-{str(uuid.uuid4())[:6]}"
149
+ print(f"[{request_id}] درخواست جدید دریافت شد.")
150
+
151
+ try:
152
+ text_chunks = split_text_into_chunks(text_prompt_from_user)
153
+ full_audio_for_file = []
154
+
155
+ # *** منطق کلیدی جدید: پردازش ترتیبی و تضمینی ***
156
+ for i, chunk in enumerate(text_chunks):
157
+ # این تابع تا زمانی که موفق نشود یا تمام کلیدها را تمام نکند، تمام نمی‌شود
158
+ chunk_audio = await process_and_stream_one_chunk(
159
+ websocket, chunk, request_id, i + 1, len(text_chunks)
160
+ )
161
+ full_audio_for_file.append(chunk_audio)
162
+
163
+ # این بخش فقط در صورتی اجرا می‌شود که تمام قطعات با موفقیت پردازش شده باشند
164
+ filename = f"{request_id}.wav"
165
+ filepath = os.path.join(TEMP_DIR, filename)
166
+ with wave.open(filepath, 'wb') as wf:
167
+ wf.setnchannels(CHANNELS); wf.setsampwidth(SAMPLE_WIDTH)
168
+ wf.setframerate(SAMPLE_RATE); wf.writeframes(b"".join(full_audio_for_file))
169
+ print(f"[{request_id}] فایل صوتی کامل در {filepath} ذخیره شد.")
170
+ await websocket.send_json({"event": "STREAM_ENDED", "url": f"/audio/{filename}"})
171
+
172
+ print(f"[{request_id}] استریم با موفقیت به پایان رسید.")
173
+
174
+ except Exception as e:
175
+ # این خطا زمانی رخ می‌دهد که یک قطعه با تمام کلیدها ناموفق باشد
176
+ error_message = f"خطای بحرانی: {e}"
177
+ print(f"[{request_id}] {error_message}")
178
+ await websocket.send_json({"event": "ERROR", "message": str(e)})
179
+
180
+ except WebSocketDisconnect:
181
+ print(f"کلاینت [{client_id}] اتصال را قطع کرد.")
182
+ except Exception as e:
183
+ print(f"[{client_id}] یک خطای پیش‌بینی‌نشده در WebSocket رخ داد: {e}")
184
+ finally:
185
+ print(f"ارتباط با کلاینت [{client_id}] بسته شد.")
186
+
187
+ # --- END OF FILE app.py ---