Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,7 @@ import time
|
|
| 8 |
import zipfile
|
| 9 |
from google import genai
|
| 10 |
from google.genai import types
|
|
|
|
| 11 |
|
| 12 |
try:
|
| 13 |
from pydub import AudioSegment
|
|
@@ -23,28 +24,43 @@ while os.environ.get(f"GEMINI_API_KEY_{i}"):
|
|
| 23 |
i += 1
|
| 24 |
|
| 25 |
NUM_API_KEYS = len(GEMINI_API_KEYS)
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
|
| 28 |
def _log(message):
|
| 29 |
-
"""تابع ساده شده برای لاگ کردن پیامها به کنسول."""
|
| 30 |
print(f"[لاگ آلفا TTS] {message}")
|
| 31 |
|
| 32 |
if NUM_API_KEYS == 0:
|
| 33 |
_log("⛔️ خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!")
|
| 34 |
-
_log(" لطفاً Secret ها را مانند GEMINI_API_KEY_1, GEMINI_API_KEY_2, ... در تنظیمات Space خود اضافه کنید.")
|
| 35 |
else:
|
| 36 |
_log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
|
| 37 |
|
| 38 |
-
def
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
if NUM_API_KEYS == 0:
|
| 41 |
-
|
| 42 |
-
return None, -1
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
# --- END: منطق چرخش API Key ---
|
| 49 |
|
| 50 |
SPEAKER_VOICES = [
|
|
@@ -56,7 +72,8 @@ SPEAKER_VOICES = [
|
|
| 56 |
]
|
| 57 |
FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
|
| 58 |
DEFAULT_MAX_CHUNK_SIZE = 3800
|
| 59 |
-
DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
|
|
|
|
| 60 |
DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
|
| 61 |
|
| 62 |
def save_binary_file(file_name, data):
|
|
@@ -91,33 +108,28 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
|
|
| 91 |
def smart_text_split(text, max_size=3800):
|
| 92 |
if len(text) <= max_size: return [text]
|
| 93 |
chunks, current_chunk = [], ""
|
| 94 |
-
# بهبود regex برای پشتیبانی بهتر از جداکنندههای فارسی و انگلیسی
|
| 95 |
sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
|
| 96 |
for sentence in sentences:
|
| 97 |
-
if len(current_chunk) + len(sentence) + 1 > max_size:
|
| 98 |
if current_chunk: chunks.append(current_chunk.strip())
|
| 99 |
current_chunk = sentence
|
| 100 |
-
while len(current_chunk) > max_size:
|
| 101 |
-
# Try to split at sensible places like comma, semicolon, or space
|
| 102 |
split_idx = -1
|
| 103 |
-
# Prefer splitting at punctuation, then space
|
| 104 |
for char_to_find in ['،', ',', ';', ':', ' ']:
|
| 105 |
try:
|
| 106 |
-
# Search backwards from max_size towards middle
|
| 107 |
split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size)
|
| 108 |
break
|
| 109 |
except ValueError:
|
| 110 |
continue
|
| 111 |
-
|
| 112 |
if split_idx != -1:
|
| 113 |
part = current_chunk[:split_idx+1]
|
| 114 |
current_chunk = current_chunk[split_idx+1:]
|
| 115 |
-
else:
|
| 116 |
part = current_chunk[:max_size]
|
| 117 |
current_chunk = current_chunk[max_size:]
|
| 118 |
chunks.append(part.strip())
|
| 119 |
else:
|
| 120 |
-
if current_chunk:
|
| 121 |
current_chunk += " " + sentence
|
| 122 |
else:
|
| 123 |
current_chunk = sentence
|
|
@@ -142,77 +154,124 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 142 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
| 143 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
| 144 |
|
| 145 |
-
selected_api_key, key_idx_display = get_next_api_key()
|
| 146 |
-
|
| 147 |
-
if not selected_api_key:
|
| 148 |
-
_log("❌ کلید API برای این درخواست در دسترس نیست. لطفاً از تنظیمات Secrets مطمئن شوید.")
|
| 149 |
-
return None
|
| 150 |
-
|
| 151 |
-
_log(f"⚙️ استفاده از کلید API شماره {key_idx_display} (پایان یافته با: ...{selected_api_key[-4:]})")
|
| 152 |
-
|
| 153 |
-
try:
|
| 154 |
-
client = genai.Client(api_key=selected_api_key)
|
| 155 |
-
except Exception as e:
|
| 156 |
-
_log(f"❌ خطا در مقداردهی اولیه کلاینت Gemini با کلید شماره {key_idx_display}: {e}")
|
| 157 |
-
return None
|
| 158 |
-
|
| 159 |
if not text_input or not text_input.strip():
|
| 160 |
_log("❌ متن ورودی خالی است.")
|
|
|
|
| 161 |
return None
|
| 162 |
|
| 163 |
text_chunks = smart_text_split(text_input, max_chunk)
|
| 164 |
if not text_chunks:
|
| 165 |
_log("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
|
|
|
|
| 166 |
return None
|
| 167 |
|
| 168 |
generated_files = []
|
| 169 |
-
|
| 170 |
-
# --- START: تغییر نحوه ترکیب prompt و chunk ---
|
| 171 |
-
if prompt_input and prompt_input.strip():
|
| 172 |
-
processed_prompt = prompt_input.strip()
|
| 173 |
-
# اگر سبک گفتار با نقطه گذاری تمام نمیشود، یک ویرگول یا نقطه اضافه میکنیم
|
| 174 |
-
# این به مدل کمک میکند تا آن را به عنوان یک عبارت راهنما قبل از متن اصلی تشخیص دهد
|
| 175 |
-
if not re.search(r'[.!?؟،:۔]$', processed_prompt):
|
| 176 |
-
processed_prompt += "،" # افزودن ویرگول فارسی به عنوان جداکننده ملایم
|
| 177 |
-
final_text = f"{processed_prompt} {chunk.strip()}"
|
| 178 |
-
else:
|
| 179 |
-
final_text = chunk.strip()
|
| 180 |
-
# --- END: تغییر نحوه ترکیب prompt و chunk ---
|
| 181 |
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
return None
|
| 213 |
|
|
|
|
| 214 |
final_audio_file = None
|
| 215 |
-
final_output_path_base = f"{
|
| 216 |
|
| 217 |
if len(generated_files) > 1:
|
| 218 |
if PYDUB_AVAILABLE:
|
|
@@ -229,7 +288,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 229 |
os.rename(generated_files[0], renamed_first_chunk)
|
| 230 |
final_audio_file = renamed_first_chunk
|
| 231 |
except Exception as e_rename:
|
| 232 |
-
_log(f"خطا در تغییر نام
|
| 233 |
final_audio_file = generated_files[0]
|
| 234 |
|
| 235 |
for fp_cleanup in generated_files:
|
|
@@ -264,13 +323,13 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 264 |
final_audio_file = generated_files[0]
|
| 265 |
|
| 266 |
if final_audio_file and os.path.exists(final_audio_file):
|
| 267 |
-
_log(f"✅ فایل صوتی نهایی با موفقیت
|
| 268 |
elif final_audio_file:
|
| 269 |
-
_log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!
|
| 270 |
return None
|
| 271 |
else:
|
| 272 |
# این حالت نباید رخ دهد اگر generated_files خالی نباشد و خطایی در تغییر نام رخ ندهد
|
| 273 |
-
_log(f"❓ وضعیت نامشخص برای فایل نهایی
|
| 274 |
return None
|
| 275 |
|
| 276 |
return final_audio_file
|
|
@@ -280,7 +339,6 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
|
|
| 280 |
if use_file_input:
|
| 281 |
if uploaded_file:
|
| 282 |
try:
|
| 283 |
-
# استفاده از uploaded_file.name که مسیر فایل موقت در Gradio است
|
| 284 |
with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
|
| 285 |
if not actual_text: _log("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
|
| 286 |
except Exception as e: _log(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
|
|
@@ -289,6 +347,10 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
|
|
| 289 |
actual_text = text_to_speak
|
| 290 |
if not actual_text or not actual_text.strip(): _log("❌ متن ورودی برای تبدیل خالی است."); return None
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
|
| 293 |
return final_path
|
| 294 |
|
|
@@ -373,7 +435,7 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
|
|
| 373 |
speech_prompt_tb = gr.Textbox(
|
| 374 |
label="سبک گفتار (اختیاری)",
|
| 375 |
placeholder="مثال: با لحنی شاد و پرانرژی",
|
| 376 |
-
value="با لحنی دوستانه و رسا صحبت کن.",
|
| 377 |
lines=2, elem_id="speech_prompt_alpha_v3"
|
| 378 |
)
|
| 379 |
speaker_voice_dd = gr.Dropdown(
|
|
@@ -387,7 +449,7 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
|
|
| 387 |
|
| 388 |
generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
|
| 389 |
|
| 390 |
-
output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
|
| 391 |
|
| 392 |
generate_button.click(
|
| 393 |
fn=gradio_tts_interface,
|
|
@@ -399,13 +461,17 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
|
|
| 399 |
gr.Examples(
|
| 400 |
examples=[
|
| 401 |
[False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
|
| 402 |
-
[False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبیعی و روان.", "Charon", 0.9],
|
| 403 |
-
[False, None, "آیا میتوانم سوالی از شما بپرسم؟", "با کنجکاوی", "Puck", 0.95],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
],
|
| 405 |
inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
|
| 406 |
outputs=[output_audio],
|
| 407 |
fn=gradio_tts_interface,
|
| 408 |
-
cache_examples=False
|
| 409 |
)
|
| 410 |
gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
|
| 411 |
|
|
|
|
| 8 |
import zipfile
|
| 9 |
from google import genai
|
| 10 |
from google.genai import types
|
| 11 |
+
from google.api_core import exceptions as google_exceptions # برای تشخیص دقیقتر خطای سهمیه
|
| 12 |
|
| 13 |
try:
|
| 14 |
from pydub import AudioSegment
|
|
|
|
| 24 |
i += 1
|
| 25 |
|
| 26 |
NUM_API_KEYS = len(GEMINI_API_KEYS)
|
| 27 |
+
# CURRENT_KEY_INDEX_GLOBAL: نشان دهنده *اولین* کلیدی است که برای یک درخواست کامل از Gradio باید امتحان شود.
|
| 28 |
+
# این متغیر پس از هر درخواست کامل (موفق یا ناموفق) پیش میرود.
|
| 29 |
+
CURRENT_KEY_INDEX_GLOBAL = 0
|
| 30 |
|
| 31 |
def _log(message):
|
|
|
|
| 32 |
print(f"[لاگ آلفا TTS] {message}")
|
| 33 |
|
| 34 |
if NUM_API_KEYS == 0:
|
| 35 |
_log("⛔️ خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!")
|
|
|
|
| 36 |
else:
|
| 37 |
_log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
|
| 38 |
|
| 39 |
+
def get_api_key_for_attempt(attempt_within_request):
|
| 40 |
+
"""
|
| 41 |
+
کلید API را برای یک تلاش خاص *درون یک درخواست Gradio* برمیگرداند.
|
| 42 |
+
attempt_within_request از 0 شروع میشود.
|
| 43 |
+
"""
|
| 44 |
if NUM_API_KEYS == 0:
|
| 45 |
+
return None, -1, -1 # key, display_num, actual_index_in_list
|
|
|
|
| 46 |
|
| 47 |
+
# اندیس کلیدی که باید در لیست GEMINI_API_KEYS استفاده شود:
|
| 48 |
+
# (اندیس کلید شروع سراسری + تعداد تلاشهای این درخواست) % تعداد کل کلیدها
|
| 49 |
+
actual_key_index_in_list = (CURRENT_KEY_INDEX_GLOBAL + attempt_within_request) % NUM_API_KEYS
|
| 50 |
+
|
| 51 |
+
key_to_use = GEMINI_API_KEYS[actual_key_index_in_list]
|
| 52 |
+
key_display_number = actual_key_index_in_list + 1 # برای نمایش به کاربر (1-based)
|
| 53 |
+
|
| 54 |
+
return key_to_use, key_display_number, actual_key_index_in_list
|
| 55 |
+
|
| 56 |
+
def advance_global_key_index_for_next_request():
|
| 57 |
+
"""
|
| 58 |
+
پس از اتمام یک درخواست کامل Gradio (تمام قطعات آن)،
|
| 59 |
+
اندیس سراسری را برای *درخواست Gradio بعدی* پیش میبرد.
|
| 60 |
+
"""
|
| 61 |
+
global CURRENT_KEY_INDEX_GLOBAL
|
| 62 |
+
if NUM_API_KEYS > 0:
|
| 63 |
+
CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
|
| 64 |
# --- END: منطق چرخش API Key ---
|
| 65 |
|
| 66 |
SPEAKER_VOICES = [
|
|
|
|
| 72 |
]
|
| 73 |
FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
|
| 74 |
DEFAULT_MAX_CHUNK_SIZE = 3800
|
| 75 |
+
DEFAULT_SLEEP_BETWEEN_REQUESTS = 8 # کاهش زمان انتظار بین قطعات اگر از یک کلید موفق استفاده میکنیم
|
| 76 |
+
RETRY_SLEEP_AFTER_QUOTA_ERROR = 2 # زمان کوتاه انتظار قبل از تلاش با کلید بعدی
|
| 77 |
DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
|
| 78 |
|
| 79 |
def save_binary_file(file_name, data):
|
|
|
|
| 108 |
def smart_text_split(text, max_size=3800):
|
| 109 |
if len(text) <= max_size: return [text]
|
| 110 |
chunks, current_chunk = [], ""
|
|
|
|
| 111 |
sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
|
| 112 |
for sentence in sentences:
|
| 113 |
+
if len(current_chunk) + len(sentence) + 1 > max_size:
|
| 114 |
if current_chunk: chunks.append(current_chunk.strip())
|
| 115 |
current_chunk = sentence
|
| 116 |
+
while len(current_chunk) > max_size:
|
|
|
|
| 117 |
split_idx = -1
|
|
|
|
| 118 |
for char_to_find in ['،', ',', ';', ':', ' ']:
|
| 119 |
try:
|
|
|
|
| 120 |
split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size)
|
| 121 |
break
|
| 122 |
except ValueError:
|
| 123 |
continue
|
|
|
|
| 124 |
if split_idx != -1:
|
| 125 |
part = current_chunk[:split_idx+1]
|
| 126 |
current_chunk = current_chunk[split_idx+1:]
|
| 127 |
+
else:
|
| 128 |
part = current_chunk[:max_size]
|
| 129 |
current_chunk = current_chunk[max_size:]
|
| 130 |
chunks.append(part.strip())
|
| 131 |
else:
|
| 132 |
+
if current_chunk:
|
| 133 |
current_chunk += " " + sentence
|
| 134 |
else:
|
| 135 |
current_chunk = sentence
|
|
|
|
| 154 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
| 155 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
if not text_input or not text_input.strip():
|
| 158 |
_log("❌ متن ورودی خالی است.")
|
| 159 |
+
advance_global_key_index_for_next_request() # اطمینان از اینکه درخواست بعدی با کلید بعدی شروع شود
|
| 160 |
return None
|
| 161 |
|
| 162 |
text_chunks = smart_text_split(text_input, max_chunk)
|
| 163 |
if not text_chunks:
|
| 164 |
_log("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
|
| 165 |
+
advance_global_key_index_for_next_request()
|
| 166 |
return None
|
| 167 |
|
| 168 |
generated_files = []
|
| 169 |
+
all_chunks_processed = True # پرچمی برای بررسی اینکه آیا تمام قطعات موفقیت آمیز بوده اند
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
+
# --- START: منطق تلاش مجدد با کلیدهای مختلف برای هر قطعه ---
|
| 172 |
+
for chunk_idx, chunk_text in enumerate(text_chunks):
|
| 173 |
+
chunk_processed_successfully = False
|
| 174 |
+
_log(f" 🔊 پردازش قطعه {chunk_idx + 1}/{len(text_chunks)}...")
|
| 175 |
|
| 176 |
+
# تعداد تلاشها برای این قطعه خاص، حداکثر به تعداد کلیدهای موجود یا 1 اگر کلیدی نیست
|
| 177 |
+
max_attempts_for_chunk = NUM_API_KEYS if NUM_API_KEYS > 0 else 1
|
| 178 |
+
|
| 179 |
+
for attempt_num_for_chunk in range(max_attempts_for_chunk):
|
| 180 |
+
selected_api_key, key_display_num, actual_key_idx = get_api_key_for_attempt(attempt_num_for_chunk)
|
| 181 |
+
|
| 182 |
+
if not selected_api_key: # اگر هیچ کلیدی موجود نباشد (نباید اینجا اتفاق بیفتد اگر NUM_API_KEYS > 0)
|
| 183 |
+
_log("❌ هیچ کلید API معتبری برای تلاش وجود ندارد.")
|
| 184 |
+
all_chunks_processed = False
|
| 185 |
+
break # خروج از حلقه تلاش برای این قطعه
|
| 186 |
+
|
| 187 |
+
_log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} برای قطعه {chunk_idx+1} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
client = genai.Client(api_key=selected_api_key)
|
| 191 |
+
|
| 192 |
+
if prompt_input and prompt_input.strip():
|
| 193 |
+
processed_prompt = prompt_input.strip()
|
| 194 |
+
if not re.search(r'[.!?؟،:۔]$', processed_prompt):
|
| 195 |
+
processed_prompt += "،"
|
| 196 |
+
final_text_for_api = f"{processed_prompt} {chunk_text.strip()}"
|
| 197 |
+
else:
|
| 198 |
+
final_text_for_api = chunk_text.strip()
|
| 199 |
+
|
| 200 |
+
# _log(f" متن ارسالی به API: '{final_text_for_api[:70]}...'")
|
| 201 |
+
|
| 202 |
+
contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
|
| 203 |
+
config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
|
| 204 |
+
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
|
| 205 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
|
| 206 |
+
|
| 207 |
+
fname_base = f"{output_base_name}_part{chunk_idx+1:03d}" # نام فایل موقت برای این قطعه
|
| 208 |
+
|
| 209 |
+
response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
|
| 210 |
+
|
| 211 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
|
| 212 |
+
inline_data = response.candidates[0].content.parts[0].inline_data
|
| 213 |
+
data_buffer = inline_data.data
|
| 214 |
+
ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
|
| 215 |
+
if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
|
| 216 |
+
if not ext.startswith("."): ext = "." + ext
|
| 217 |
+
|
| 218 |
+
# اطمینان از اینکه فایل قبلی (اگر از تلاش قبلی برای همین قطعه مانده) حذف شود
|
| 219 |
+
temp_fpath_for_chunk = f"{fname_base}{ext}"
|
| 220 |
+
if os.path.exists(temp_fpath_for_chunk):
|
| 221 |
+
try: os.remove(temp_fpath_for_chunk)
|
| 222 |
+
except OSError as e_rem: _log(f" ⚠️ نتوانست فایل موقت قبلی را حذف کند: {e_rem}")
|
| 223 |
+
|
| 224 |
+
fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
|
| 225 |
+
if fpath:
|
| 226 |
+
generated_files.append(fpath)
|
| 227 |
+
chunk_processed_successfully = True
|
| 228 |
+
_log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
|
| 229 |
+
if chunk_idx < len(text_chunks) - 1: # اگر قطعات دیگری هم هستند
|
| 230 |
+
time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS) # صبر قبل از پردازش قطعه بعدی (با همین کلید)
|
| 231 |
+
break # خروج از حلقه تلاش (attempt_num_for_chunk) چون این قطعه موفق بود
|
| 232 |
+
else:
|
| 233 |
+
_log(f" ⚠️ پاسخ API برای قطعه {chunk_idx+1} با کلید {key_display_num} بدون داده صوتی بود.")
|
| 234 |
+
|
| 235 |
+
except google_exceptions.ResourceExhausted as e_quota:
|
| 236 |
+
_log(f" ❌ خطای سهمیه (RESOURCE_EXHAUSTED) برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {e_quota.message[:100]}...")
|
| 237 |
+
if attempt_num_for_chunk < max_attempts_for_chunk - 1: # اگر کلیدهای دیگری برای امتحان باقی مانده
|
| 238 |
+
_log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه.")
|
| 239 |
+
time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
|
| 240 |
+
else:
|
| 241 |
+
_log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند و ناموفق بودند (خطای سهمیه).")
|
| 242 |
+
all_chunks_processed = False # یک قطعه ناموفق بود
|
| 243 |
+
|
| 244 |
+
except Exception as e_general:
|
| 245 |
+
_log(f" ❌ خطای عمومی در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {e_general}")
|
| 246 |
+
# برای خطاهای عمومی، معمولاً تلاش مجدد با کلید دیگر کمکی نمیکند، مگر اینکه خطای شبکه موقتی باشد
|
| 247 |
+
# اما برای سادگی، اجازه میدهیم حلقه تلاش ادامه یابد
|
| 248 |
+
if attempt_num_for_chunk < max_attempts_for_chunk - 1:
|
| 249 |
+
time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR) # کمی صبر قبل از تلاش با کلید بعدی
|
| 250 |
+
else:
|
| 251 |
+
all_chunks_processed = False # یک قطعه ناموفق بود
|
| 252 |
+
|
| 253 |
+
if chunk_processed_successfully:
|
| 254 |
+
break # از حلقه تلاش برای این قطعه خارج شو
|
| 255 |
+
|
| 256 |
+
if not chunk_processed_successfully:
|
| 257 |
+
_log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود.")
|
| 258 |
+
all_chunks_processed = False
|
| 259 |
+
break # خروج از حلقه اصلی پردازش قطعات (chunk_idx) چون یک قطعه حیاتی ناموفق بود
|
| 260 |
+
# --- END: منطق تلاش مجدد ---
|
| 261 |
+
|
| 262 |
+
advance_global_key_index_for_next_request() # برای درخواست Gradio بعدی، از کلید بعدی شروع کن
|
| 263 |
+
|
| 264 |
+
if not all_chunks_processed or not generated_files:
|
| 265 |
+
_log("❌ هیچ فایل صوتی معتبری تولید نشد (ممکن است برخی قطعات ناموفق بوده باشند یا سهمیه تمام کلیدها تمام شده باشد).")
|
| 266 |
+
# پاک کردن فایلهای جزئی ایجاد شده اگر فرآیند کامل نشده
|
| 267 |
+
for fp in generated_files:
|
| 268 |
+
try: os.remove(fp)
|
| 269 |
+
except: pass
|
| 270 |
return None
|
| 271 |
|
| 272 |
+
# _log(f"🎉 {len(generated_files)} فایل(های) صوتی خام تولید شد.") # لاگ کمتر
|
| 273 |
final_audio_file = None
|
| 274 |
+
final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
|
| 275 |
|
| 276 |
if len(generated_files) > 1:
|
| 277 |
if PYDUB_AVAILABLE:
|
|
|
|
| 288 |
os.rename(generated_files[0], renamed_first_chunk)
|
| 289 |
final_audio_file = renamed_first_chunk
|
| 290 |
except Exception as e_rename:
|
| 291 |
+
_log(f"خطا در تغییر نام اولین قطعه (پس از ادغام ناموفق): {e_rename}")
|
| 292 |
final_audio_file = generated_files[0]
|
| 293 |
|
| 294 |
for fp_cleanup in generated_files:
|
|
|
|
| 323 |
final_audio_file = generated_files[0]
|
| 324 |
|
| 325 |
if final_audio_file and os.path.exists(final_audio_file):
|
| 326 |
+
_log(f"✅ فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_audio_file)}")
|
| 327 |
elif final_audio_file:
|
| 328 |
+
_log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!")
|
| 329 |
return None
|
| 330 |
else:
|
| 331 |
# این حالت نباید رخ دهد اگر generated_files خالی نباشد و خطایی در تغییر نام رخ ندهد
|
| 332 |
+
_log(f"❓ وضعیت نامشخص برای فایل نهایی.")
|
| 333 |
return None
|
| 334 |
|
| 335 |
return final_audio_file
|
|
|
|
| 339 |
if use_file_input:
|
| 340 |
if uploaded_file:
|
| 341 |
try:
|
|
|
|
| 342 |
with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
|
| 343 |
if not actual_text: _log("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
|
| 344 |
except Exception as e: _log(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
|
|
|
|
| 347 |
actual_text = text_to_speak
|
| 348 |
if not actual_text or not actual_text.strip(): _log("❌ متن ورودی برای تبدیل خالی است."); return None
|
| 349 |
|
| 350 |
+
if NUM_API_KEYS == 0: # بررسی اولیه قبل از فراخوانی core_generate_audio
|
| 351 |
+
_log("❌ هیچ کلید API برای پردازش موجود نیست. لطفاً Secrets را بررسی کنید.")
|
| 352 |
+
return None
|
| 353 |
+
|
| 354 |
final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
|
| 355 |
return final_path
|
| 356 |
|
|
|
|
| 435 |
speech_prompt_tb = gr.Textbox(
|
| 436 |
label="سبک گفتار (اختیاری)",
|
| 437 |
placeholder="مثال: با لحنی شاد و پرانرژی",
|
| 438 |
+
value="با لحنی دوستانه و رسا صحبت کن.",
|
| 439 |
lines=2, elem_id="speech_prompt_alpha_v3"
|
| 440 |
)
|
| 441 |
speaker_voice_dd = gr.Dropdown(
|
|
|
|
| 449 |
|
| 450 |
generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
|
| 451 |
|
| 452 |
+
output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
|
| 453 |
|
| 454 |
generate_button.click(
|
| 455 |
fn=gradio_tts_interface,
|
|
|
|
| 461 |
gr.Examples(
|
| 462 |
examples=[
|
| 463 |
[False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
|
| 464 |
+
[False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.", "با صدایی طبیعی و روان.", "Charon", 0.9],
|
| 465 |
+
[False, None, "آیا میتوانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.", "با کنجکاوی", "Puck", 0.95],
|
| 466 |
+
# یک نمونه طولانیتر برای تست تقسیم به چند قطعه و چرخش کلید
|
| 467 |
+
[False, None,
|
| 468 |
+
"این یک متن بسیار طولانی است که به احتمال زیاد به چندین قطعه تقسیم خواهد شد. هدف از این نمونه، بررسی عملکرد صحیح تقسیم متن و همچنین آزمایش مکانیزم چرخش کلید API در صورتی که سهمیه یک کلید در حین پردازش تمام شود، میباشد. امیدواریم که برنامه بتواند به طور خودکار به کلید بعدی سوئیچ کرده و فرآیند تولید صدا را با موفقیت به اتمام برساند. این بخش اول است. این بخش دوم است. و این هم بخش سوم برای طولانیتر کردن متن.",
|
| 469 |
+
"با لحنی آرام و واضح", "Achird", 0.8],
|
| 470 |
],
|
| 471 |
inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
|
| 472 |
outputs=[output_audio],
|
| 473 |
fn=gradio_tts_interface,
|
| 474 |
+
cache_examples=False
|
| 475 |
)
|
| 476 |
gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
|
| 477 |
|