Update app.py
Browse files
app.py
CHANGED
|
@@ -8,11 +8,11 @@ import time
|
|
| 8 |
import zipfile
|
| 9 |
import importlib.metadata
|
| 10 |
|
| 11 |
-
# --- START: Import کتابخانههای گوگل ---
|
| 12 |
GOOGLE_LIBS_AVAILABLE = False
|
| 13 |
-
GENAI_MODEL_ACCESS_CONFIGURED = False
|
| 14 |
|
| 15 |
-
def _log_startup(message):
|
| 16 |
print(f"[Startup Log] {message}")
|
| 17 |
|
| 18 |
try:
|
|
@@ -24,14 +24,13 @@ try:
|
|
| 24 |
except importlib.metadata.PackageNotFoundError:
|
| 25 |
_log_startup("هشدار: پکیج 'google-generativeai' نصب شده، اما نسخهی آن قابل تشخیص نیست.")
|
| 26 |
|
| 27 |
-
# در نسخههای جدید، Client وجود ندارد، به جای آن GenerativeModel و configure استفاده میشود
|
| 28 |
if hasattr(genai, 'GenerativeModel') and hasattr(genai, 'configure'):
|
| 29 |
_log_startup("ویژگیهای 'GenerativeModel' و 'configure' در ماژول 'genai' یافت شدند.")
|
| 30 |
-
GENAI_MODEL_ACCESS_CONFIGURED = True
|
| 31 |
else:
|
| 32 |
-
_log_startup("⛔️ خطای مهم: 'GenerativeModel' یا 'configure' در 'genai' یافت نشد.
|
| 33 |
|
| 34 |
-
from google.generativeai import types
|
| 35 |
from google.api_core import exceptions as google_exceptions
|
| 36 |
_log_startup("'types' و 'google_exceptions' با موفقیت وارد شدند.")
|
| 37 |
GOOGLE_LIBS_AVAILABLE = True
|
|
@@ -42,8 +41,14 @@ except Exception as e_other:
|
|
| 42 |
_log_startup(f"❌ خطای ناشناخته در حین import یا بررسی کتابخانههای گوگل: {e_other}")
|
| 43 |
# --- END: Import کتابخانههای گوگل ---
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
GEMINI_API_KEYS = []
|
| 48 |
i = 1
|
| 49 |
while os.environ.get(f"GEMINI_API_KEY_{i}"):
|
|
@@ -51,12 +56,12 @@ while os.environ.get(f"GEMINI_API_KEY_{i}"):
|
|
| 51 |
i += 1
|
| 52 |
NUM_API_KEYS = len(GEMINI_API_KEYS)
|
| 53 |
CURRENT_KEY_INDEX_GLOBAL = 0
|
| 54 |
-
def _log(message):
|
| 55 |
print(f"[لاگ آلفا TTS] {message}")
|
| 56 |
if not GOOGLE_LIBS_AVAILABLE:
|
| 57 |
_log("🔴 به دلیل عدم بارگذاری کتابخانههای اصلی گوگل، عملکرد برنامه مختل خواهد شد.")
|
| 58 |
if NUM_API_KEYS == 0:
|
| 59 |
-
_log("⛔️ هشدار: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!
|
| 60 |
else:
|
| 61 |
_log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
|
| 62 |
def get_api_key_for_attempt(attempt_within_request):
|
|
@@ -69,12 +74,6 @@ def advance_global_key_index_for_next_request():
|
|
| 69 |
global CURRENT_KEY_INDEX_GLOBAL
|
| 70 |
if NUM_API_KEYS > 0: CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
|
| 71 |
# --- END: منطق چرخش API Key ---
|
| 72 |
-
try:
|
| 73 |
-
from pydub import AudioSegment
|
| 74 |
-
PYDUB_AVAILABLE = True
|
| 75 |
-
except ImportError:
|
| 76 |
-
_log_startup("⚠️ کتابخانه pydub یافت نشد. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
| 77 |
-
PYDUB_AVAILABLE = False
|
| 78 |
|
| 79 |
SPEAKER_VOICES = [
|
| 80 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
|
@@ -83,39 +82,17 @@ SPEAKER_VOICES = [
|
|
| 83 |
"Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
|
| 84 |
"Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
| 85 |
]
|
| 86 |
-
|
| 87 |
-
# نام مدل
|
| 88 |
-
# ی
|
| 89 |
-
#
|
| 90 |
-
|
| 91 |
-
# اگر از مدل پایه Flash استفاده میکنیم، باید قابلیت TTS آن را فعال کنیم.
|
| 92 |
-
# فعلاً از نام مدل قبلی استفاده میکنیم و امیدواریم با configure کار کند.
|
| 93 |
-
# ** مهم: نام مدل TTS در API جدید ممکن است "models/tts-1" یا چیزی شبیه به این باشد. باید داکیومنت API v1beta را برای TTS بررسی کرد.
|
| 94 |
-
# با توجه به اینکه قبلا از "gemini-2.5-flash-preview-tts" استفاده میکردید، احتمالاً برای API جدید
|
| 95 |
-
# باید از "models/gemini-1.5-flash" (یا مشابه) به همراه تنظیمات TTS استفاده کنید.
|
| 96 |
-
# فعلاً "models/gemini-1.5-flash" را فرض میکنیم و generation_config را برای TTS تنظیم میکنیم.
|
| 97 |
-
# **اصلاح مهم: مدل TTS هنوز در API اصلی به طور عمومی در دسترس نیست و ممکن است نیاز به endpoint خاصی داشته باشد یا از طریق Vertex AI قابل دسترس باشد.**
|
| 98 |
-
# **با فرض اینکه مدل TTS مانند قبل در دسترس است، اما با API جدید:**
|
| 99 |
-
# **مدل صحیح برای TTS با API جدیدتر احتمالاً چیزی شبیه به این است:**
|
| 100 |
-
# FIXED_MODEL_NAME_TTS = "models/tts-1" # یا نامی که در داکیومنت جدید برای TTS آمده
|
| 101 |
-
# یا استفاده از مدل پایه با قابلیتهای خاص:
|
| 102 |
-
FIXED_MODEL_NAME_FOR_TTS_API_V1 = "gemini-1.5-flash" # یا "gemini-pro" اگر TTS دارند
|
| 103 |
-
# فعلا با همان نام مدل قبلی شما پیش میرویم و امیدواریم با API جدید کار کند:
|
| 104 |
-
# FIXED_MODEL_NAME_ACTUAL = "gemini-2.5-flash-preview-tts" # این نام برای API قبلی بود
|
| 105 |
-
# برای API جدید (که Client ندارد)، باید از نام مدلهای استاندارد استفاده کرد.
|
| 106 |
-
# **به نظر میرسد مستقیمترین راه برای TTS با API جدید از طریق `genai.GenerativeModel('models/gemini-1.5-flash-latest')` و تنظیم `response_mime_type='audio/ogg'` در `generation_config` است.**
|
| 107 |
-
# یا یک مدل خاص TTS اگر تعریف شده.
|
| 108 |
-
# بیایید فرض کنیم یک مدل به نام "tts-model" یا مشابه در دسترس است.
|
| 109 |
-
# فعلاً از نام مدل قبلی شما استفاده میکنیم و به جای client.models... از model... استفاده خواهیم کرد.
|
| 110 |
-
# **اگر مدل "gemini-2.5-flash-preview-tts" با API جدید (که Client ندارد) کار نکند، باید به دنبال نام مدل TTS معادل در API جدید بگردید.**
|
| 111 |
-
# **مهمترین تغییر این است که `client.models.generate_content` به `model.generate_content` تبدیل میشود.**
|
| 112 |
|
| 113 |
DEFAULT_MAX_CHUNK_SIZE = 3800
|
| 114 |
DEFAULT_SLEEP_BETWEEN_REQUESTS = 6
|
| 115 |
RETRY_SLEEP_AFTER_QUOTA_ERROR = 2
|
| 116 |
DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
|
| 117 |
|
| 118 |
-
# ... (توابع save_binary_file, convert_to_wav, parse_audio_mime_type, smart_text_split, merge_audio_files_func بدون تغییر) ...
|
| 119 |
def save_binary_file(file_name, data):
|
| 120 |
try:
|
| 121 |
with open(file_name, "wb") as f: f.write(data)
|
|
@@ -123,7 +100,7 @@ def save_binary_file(file_name, data):
|
|
| 123 |
except Exception as e:
|
| 124 |
_log(f"❌ خطا در ذخیره فایل {file_name}: {e}")
|
| 125 |
return None
|
| 126 |
-
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
| 127 |
parameters = parse_audio_mime_type(mime_type)
|
| 128 |
bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
|
| 129 |
num_channels, data_size = 1, len(audio_data)
|
|
@@ -138,7 +115,7 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
|
|
| 138 |
if param_lower_startswith(param, "rate="):
|
| 139 |
try: rate = int(param.split("=", 1)[1])
|
| 140 |
except: pass
|
| 141 |
-
elif param.startswith("audio/L"):
|
| 142 |
try: bits = int(param.split("L", 1)[1])
|
| 143 |
except: pass
|
| 144 |
return {"bits_per_sample": bits, "rate": rate}
|
|
@@ -169,8 +146,7 @@ def merge_audio_files_func(file_paths, output_path):
|
|
| 169 |
combined.export(output_path, format="wav"); return True
|
| 170 |
except Exception as e: _log(f"❌ خطا در ادغام: {e}"); return False
|
| 171 |
|
| 172 |
-
|
| 173 |
-
def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
|
| 174 |
if not GOOGLE_LIBS_AVAILABLE or not GENAI_MODEL_ACCESS_CONFIGURED:
|
| 175 |
_log("❌ کتابخانههای گوگل یا تنظیمات مدل به درستی بارگذاری نشدهاند.")
|
| 176 |
return None
|
|
@@ -182,17 +158,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 182 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
| 183 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
| 184 |
|
| 185 |
-
|
| 186 |
-
# در اینجا از FIXED_MODEL_NAME_FOR_TTS_API_V1 استفاده میکنیم که باید نام یک مدل پایه باشد.
|
| 187 |
-
# اگر یک مدل خاص TTS مانند "models/text-to-speech" وجود دارد، از آن استفاده کنید.
|
| 188 |
-
# در حال حاضر، از نام مدل قبلی شما ("gemini-2.5-flash-preview-tts") استفاده میکنیم،
|
| 189 |
-
# و امیدواریم که با ساختار API جدید (بدون Client) کار کند.
|
| 190 |
-
# اگر کار نکرد، باید نام مدل را به یکی از مدلهای استاندارد مانند "gemini-1.5-flash-latest" تغییر دهید
|
| 191 |
-
# و generation_config را برای خروجی صوتی تنظیم کنید.
|
| 192 |
-
# **برای سادگی و تست اولیه، فرض میکنیم نام مدل قبلی هنوز معتبر است.**
|
| 193 |
-
model_name_to_use = "gemini-1.5-flash-latest" # این یک مدل پایه است، برای TTS باید config خاصی داشته باشد.
|
| 194 |
-
# یا اگر مدل قبلی شما هنوز کار میکند:
|
| 195 |
-
# model_name_to_use = "gemini-2.5-flash-preview-tts" # این نام از API قبلی است
|
| 196 |
|
| 197 |
if not text_input or not text_input.strip():
|
| 198 |
_log("❌ متن ورودی خالی است."); advance_global_key_index_for_next_request(); return None
|
|
@@ -213,156 +179,46 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 213 |
_log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
|
| 214 |
|
| 215 |
try:
|
| 216 |
-
# --- تغییر کلیدی: تنظیم API Key و ایجاد مدل ---
|
| 217 |
genai.configure(api_key=selected_api_key)
|
| 218 |
-
# model = genai.GenerativeModel(model_name_to_use) # برای مدلهای پایه
|
| 219 |
-
# برای TTS ممکن است نیاز به مدل خاص یا تنظیمات خاص باشد.
|
| 220 |
-
# فعلا از نام مدل قبلی استفاده میکنیم، اگر با API جدید کار کند:
|
| 221 |
-
# ** اگر "gemini-2.5-flash-preview-tts" با API جدید کار نمیکند، این بخش باید تغییر کند **
|
| 222 |
-
# ** به احتمال زیاد باید از یک مدل پایه (مانند gemini-1.5-flash-latest) و GenerationConfig برای TTS استفاده کرد **
|
| 223 |
-
|
| 224 |
-
# استفاده از نام مدل قبلی شما با این فرض که با API جدید هم کار میکند
|
| 225 |
-
# این بخش نیاز به تست و احتمالا اصلاح نام مدل دارد.
|
| 226 |
model_instance = genai.GenerativeModel(model_name_to_use)
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
| 231 |
if not re.search(r'[.!?؟،:۔]$', processed_prompt): processed_prompt += "،"
|
| 232 |
-
final_text_for_api = f"{processed_prompt} {
|
| 233 |
-
else: final_text_for_api = chunk_text.strip()
|
| 234 |
-
|
| 235 |
-
# تنظیمات برای خروجی صوتی (این بخش ممکن است نیاز به تنظیم دقیقتر بر اساس داکیومنت API جدید داشته باشد)
|
| 236 |
-
# این config از کد قبلی شما میآید و برای API جدید باید سازگار باشد.
|
| 237 |
-
# ** مهم: `response_modalities` در API جدید با `response_mime_type` جایگزین شده است. **
|
| 238 |
-
generation_config_tts = types.GenerationConfig(
|
| 239 |
-
temperature=temperature_val,
|
| 240 |
-
# response_modalities=["audio"], # این برای API قدیمی بود
|
| 241 |
-
response_mime_type="audio/wav", # یا audio/ogg - برای API جدید
|
| 242 |
-
candidate_count=1 # معمولا برای TTS یک کاندید کافی است
|
| 243 |
-
)
|
| 244 |
-
# speech_config هنوز ممکن است معتبر باشد یا به generation_config منتقل شده باشد.
|
| 245 |
-
# فعلا فرض میکنیم SpeechConfig جداگانه هنوز استفاده میشود.
|
| 246 |
-
# ** این بخش نیاز به بررسی داکیومنت API v1 (یا جدیدتر) دارد. **
|
| 247 |
-
# ** به نظر میرسد SpeechConfig دیگر به این شکل مستقیم در generate_content نیست **
|
| 248 |
-
# ** و تنظیمات صدا باید بخشی از prompt یا generation_config باشند. **
|
| 249 |
-
# ** برای سادگی، فعلا speech_config را حذف میکنیم و به تنظیمات پایه اکتفا میکنیم **
|
| 250 |
-
# ** و امیدواریم مدل TTS به طور پیشفرض صدای مناسبی تولید کند یا بتوانیم با prompt آن را کنترل کنیم. **
|
| 251 |
-
|
| 252 |
-
# response = model_instance.generate_content(
|
| 253 |
-
# contents=final_text_for_api, # API جدید معمولا contents را به عنوان رشته یا لیست رشتهها میپذیرد
|
| 254 |
-
# generation_config=generation_config_tts,
|
| 255 |
-
# # speech_config=types.SpeechConfig(...) # این احتمالا دیگر کار نمیکند
|
| 256 |
-
# )
|
| 257 |
-
# ** روش قدیمیتر ارسال content با types.Content **
|
| 258 |
-
contents_payload = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
|
| 259 |
-
# ** این generation_config از کد قبلی شما میآید **
|
| 260 |
-
original_config_from_your_code = types.GenerateContentConfig(
|
| 261 |
-
temperature=temperature_val,
|
| 262 |
-
response_modalities=["audio"], # این باید به response_mime_type تغییر کند
|
| 263 |
-
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
|
| 264 |
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
|
| 265 |
-
)
|
| 266 |
-
# ** تطبیق با API جدید **
|
| 267 |
-
# در API جدید، `response_modalities` وجود ندارد. به جای آن `response_mime_type` در `GenerationConfig` اصلی قرار میگیرد.
|
| 268 |
-
# `speech_config` هم ممکن است مستقیماً در `generate_content` نباشد.
|
| 269 |
-
# فعلاً از ساختار قبلی شما با کمی تغییر برای `generate_content` مدل جدید استفاده میکنیم.
|
| 270 |
-
|
| 271 |
-
# --- روش جدیدتر برای TTS با مدل پایه (مثل Flash) ---
|
| 272 |
-
# این روش استانداردتر برای API جدید است اگر مدل پایه قابلیت TTS دارد.
|
| 273 |
-
# شما باید voice و سایر تنظیمات را از طریق prompt یا تنظیمات خاص مدل انجام دهید.
|
| 274 |
-
# فعلاً فرض میکنیم که مدل `model_name_to_use` به طور مستقیم از TTS پشتیبانی میکند
|
| 275 |
-
# و `speech_config` هنوز معتبر است.
|
| 276 |
-
# این بخش بحرانی است و نیاز به تطبیق با داکیومنت دقیق API جدید دارد.
|
| 277 |
-
# ** اگر از مدل پایه مثل gemini-1.5-flash-latest استفاده میکنید، به احتمال زیاد speech_config مستقیم کار نمیکند **
|
| 278 |
-
# ** و باید از طریق prompt یا generation_config خاص TTS عمل کنید. **
|
| 279 |
-
|
| 280 |
-
# ** تلاش برای استفاده از ساختار قبلی شما با مدل جدید، با این امید که کار کند **
|
| 281 |
-
# این فقط یک حدس است و ممکن است نیاز به تغییرات اساسی داشته باشد.
|
| 282 |
-
# مهمترین تغییر `client.models.generate_content` به `model_instance.generate_content` است.
|
| 283 |
|
| 284 |
-
#
|
| 285 |
-
#
|
| 286 |
-
#
|
| 287 |
-
|
| 288 |
-
temperature=temperature_val,
|
| 289 |
-
# response_modalities=["audio"], # حذف شد
|
| 290 |
-
response_mime_type="audio/wav", # یا audio/ogg
|
| 291 |
-
# speech_config ممکن است دیگر اینجا نباشد
|
| 292 |
-
)
|
| 293 |
-
# اگر speech_config هنوز کار میکند:
|
| 294 |
-
# tools = [types.Tool(speech_config=types.SpeechConfig(voice_config=...))]
|
| 295 |
-
# اما به احتمال زیاد این تغییر کرده.
|
| 296 |
-
|
| 297 |
-
# ** یک فرض سادهتر: مدل خودش میداند که TTS است و فقط متن و config پایه را میخواهد **
|
| 298 |
-
# response = model_instance.generate_content(
|
| 299 |
-
# final_text_for_api,
|
| 300 |
-
# generation_config=generation_config_for_tts
|
| 301 |
-
# )
|
| 302 |
-
# ** بازگشت به تلاش برای استفاده از ساختار config قبلی شما، با تغییرات جزئی **
|
| 303 |
-
# این بخش بسیار آزمایشی است
|
| 304 |
-
final_config_attempt = types.GenerateContentConfig( # این GenerateContentConfig از google.generativeai.types است
|
| 305 |
-
temperature=temperature_val,
|
| 306 |
-
# response_modalities=["audio"], # این دیگر وجود ندارد
|
| 307 |
-
# speech_config هنوز ممکن است در برخی موارد خاص کار کند، اما بعید است
|
| 308 |
-
# speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
|
| 309 |
-
# prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
|
| 310 |
-
)
|
| 311 |
-
# برای TTS با مدلهای جدید، معمولاً به این شکل است:
|
| 312 |
-
# model = genai.GenerativeModel('models/gemini-1.5-flash') یا مدل TTS خاص
|
| 313 |
-
# response = model.generate_content(
|
| 314 |
-
# "متن شما",
|
| 315 |
-
# generation_config=genai.types.GenerationConfig(
|
| 316 |
-
# response_mime_type="audio/wav", # یا ogg
|
| 317 |
-
# # سایر پارامترهای دما و ...
|
| 318 |
-
# ),
|
| 319 |
-
# # برای کنترل صدا، ممکن است نیاز به prompt engineering باشد
|
| 320 |
-
# # یا اگر مدلی با قابلیتهای voice وجود دارد، از طریق آن.
|
| 321 |
-
# )
|
| 322 |
-
# ** با توجه به اینکه شما speech_config داشتید، باید ببینیم معادل آن در API جدید چیست **
|
| 323 |
-
# ** فعلا فرض میکنیم مدل TTS هوشمند است و فقط با متن کار میکند و تنظیمات پایه **
|
| 324 |
-
# ** این یک سادهسازی بزرگ است و احتمالاً کار نخواهد کرد بدون تنظیمات دقیق TTS **
|
| 325 |
|
| 326 |
-
|
| 327 |
-
# ** و امیدواریم مدل پیشفرض TTS صدای مناسبی بدهد. **
|
| 328 |
-
# ** این احتمالاً درست نیست و نیاز به بررسی داکیومنت API جدید برای TTS دارد. **
|
| 329 |
|
| 330 |
-
# *** مهمترین تغییر: ***
|
| 331 |
-
# از `model_instance.generate_content` استفاده میکنیم.
|
| 332 |
-
# `contents` باید یک لیست از `Part` یا رشته باشد.
|
| 333 |
-
# `generation_config` باید `types.GenerationConfig` باشد.
|
| 334 |
-
# `speech_config` در اینجا دیگر مستقیم نیست.
|
| 335 |
-
|
| 336 |
-
# ساختار سادهتر برای generate_content با API جدید:
|
| 337 |
response = model_instance.generate_content(
|
| 338 |
-
contents=final_text_for_api,
|
| 339 |
-
generation_config=types.GenerationConfig(
|
| 340 |
temperature=temperature_val,
|
| 341 |
-
response_mime_type="audio/wav"
|
| 342 |
-
# candidate_count=1 # معمولا برای TTS
|
| 343 |
)
|
| 344 |
-
# پارامتر voice_name و speech_prompt باید به نحو دیگری به مدل منتقل شوند،
|
| 345 |
-
# احتمالاً از طریق خود متن (prompt engineering) یا تنظیمات خاص مدل اگر وجود داشته باشد.
|
| 346 |
-
# این یک چالش با API جدید برای TTS است اگر تنظیمات صدا پیچیده باشند.
|
| 347 |
)
|
| 348 |
-
|
| 349 |
-
# --- END: تلاش برای تطبیق ---
|
| 350 |
|
| 351 |
-
# پردازش پاسخ (این بخش باید با ساختار پاسخ جدید API تطابق داشته باشد)
|
| 352 |
-
# در API جدید، معمولاً پاسخ مستقیم حاوی داده باینری نیست، بلکه یک URI به فایل است یا داده در Candidate.
|
| 353 |
-
# با فرض اینکه ساختار Candidate.content.parts[0].inline_data هنوز معتبر است:
|
| 354 |
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
|
| 355 |
inline_data = response.candidates[0].content.parts[0].inline_data
|
| 356 |
data_buffer = inline_data.data
|
| 357 |
-
|
| 358 |
-
#
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
# data_buffer = convert_to_wav(data_buffer, mime_type_from_response)
|
| 364 |
if not ext.startswith("."): ext = "." + ext
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
fname_base = f"{output_base_name}_part{chunk_idx+1:03d}"
|
| 367 |
temp_fpath_for_chunk = f"{fname_base}{ext}"
|
| 368 |
if os.path.exists(temp_fpath_for_chunk):
|
|
@@ -376,7 +232,10 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 376 |
if chunk_idx < len(text_chunks) - 1: time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
|
| 377 |
break
|
| 378 |
else:
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
except google_exceptions.ResourceExhausted as e_quota:
|
| 382 |
_log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
|
|
@@ -386,11 +245,15 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 386 |
|
| 387 |
except Exception as e_general:
|
| 388 |
error_type_name = type(e_general).__name__
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
|
|
|
| 393 |
_log(f" مدل '{model_name_to_use}' یافت نشد یا برای این کلید API در دسترس نیست.")
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
if attempt_num_for_chunk < max_attempts_for_chunk - 1: time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
|
| 396 |
else: all_chunks_processed = False
|
|
@@ -401,10 +264,10 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 401 |
_log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود."); all_chunks_processed = False; break
|
| 402 |
|
| 403 |
advance_global_key_index_for_next_request()
|
| 404 |
-
|
| 405 |
if not all_chunks_processed or not generated_files:
|
| 406 |
_log("❌ هیچ فایل صوتی معتبری تولید نشد.")
|
| 407 |
-
for fp_cleanup in generated_files:
|
| 408 |
try: os.remove(fp_cleanup)
|
| 409 |
except: pass
|
| 410 |
return None
|
|
@@ -421,7 +284,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 421 |
if os.path.exists(renamed_first_chunk): os.remove(renamed_first_chunk)
|
| 422 |
os.rename(generated_files[0], renamed_first_chunk); final_audio_file = renamed_first_chunk
|
| 423 |
except Exception as e_rename: _log(f"خطا در تغییر نام اولین قطعه: {e_rename}"); final_audio_file = generated_files[0]
|
| 424 |
-
for fp_cleanup_merge in generated_files:
|
| 425 |
if final_audio_file and os.path.abspath(fp_cleanup_merge) == os.path.abspath(final_audio_file): continue
|
| 426 |
try: os.remove(fp_cleanup_merge)
|
| 427 |
except: pass
|
|
@@ -447,8 +310,6 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 447 |
else: _log(f"❓ وضعیت نامشخص برای فایل نهایی."); return None
|
| 448 |
return final_audio_file
|
| 449 |
|
| 450 |
-
|
| 451 |
-
# ... (تابع gradio_tts_interface و UI و launch بدون تغییر نسبت به نسخه کامل قبلی) ...
|
| 452 |
def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
|
| 453 |
actual_text = ""
|
| 454 |
if use_file_input:
|
|
@@ -461,21 +322,25 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
|
|
| 461 |
else:
|
| 462 |
actual_text = text_to_speak
|
| 463 |
if not actual_text or not actual_text.strip(): _log("❌ متن ورودی خالی."); return None
|
| 464 |
-
|
|
|
|
| 465 |
gr.Warning("خطای سیستمی: کتابخانههای مورد نیاز یا تنظیمات مدل به درستی بارگذاری نشدهاند.")
|
| 466 |
return None
|
| 467 |
if NUM_API_KEYS == 0:
|
| 468 |
gr.Warning("خطای سیستمی: کلید API موجود نیست.")
|
| 469 |
return None
|
| 470 |
-
|
|
|
|
|
|
|
| 471 |
if final_path is None:
|
| 472 |
-
gr.Info("امکان تولید صدا وجود ندارد. لطفاً دقایقی دیگر
|
| 473 |
return final_path
|
| 474 |
|
|
|
|
| 475 |
custom_css_inspired_by_image = f"""@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');:root {{ --app-font: 'Vazirmatn', sans-serif; --app-header-grad-start: #2980b9; --app-header-grad-end: #2ecc71; --app-panel-bg: #FFFFFF; --app-input-bg: #F7F7F7; --app-button-bg: #2979FF; --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); --app-text-primary: #333; --app-text-secondary: #555; --app-border-color: #E0E0E0; --radius-card: 20px; --radius-input: 8px; --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1); --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);}}body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 16px; line-height: 1.65; }}.gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}.app-header-alpha {{ padding: 3rem 1.5rem 4rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2); }}.app-header-alpha h1 {{ font-size: 2.4em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }}.app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}.main-content-panel-alpha {{ padding: 1.8rem 1.5rem; max-width: 680px; margin: -2.5rem auto 2rem auto; width: 90%; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); position:relative; z-index:10; }}@media (max-width: 768px) {{ .main-content-panel-alpha {{ width: 95%; padding: 1.5rem 1rem; margin-top: -2rem; }} .app-header-alpha h1 {{font-size:2em;}} .app-header-alpha p {{font-size:1em;}} }}footer {{display:none !important;}}.gr-button.generate-button-final {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.8rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.3s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.5rem !important; }}.gr-button.generate-button-final:hover {{ filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba(41,121,255,0.6);}}.gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-file > label + div {{ border-radius: var(--radius-input) !important; border: 1px solid var(--app-border-color) !important; background-color: var(--app-input-bg) !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); padding: 0.75rem !important; }}.gr-file > label + div {{ text-align:center; border-style: dashed !important; }}.gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-textbox > label + div > textarea:focus {{ border-color: var(--app-button-bg) !important; box-shadow: 0 0 0 3px rgba(41,121,255,0.2) !important; }}label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}.section-title-main-alpha {{ font-size: 1.1em; color: var(--app-text-secondary); margin-bottom:1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--app-border-color); font-weight:500; text-align:right; }}label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}#output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}.temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}.app-footer-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.8; margin-top:3rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}"""
|
| 476 |
alpha_header_html_v3 = """<div class='app-header-alpha'><h1>Alpha TTS</h1><p>جادوی تبدیل متن به صدا در دستان شما</p></div>"""
|
| 477 |
|
| 478 |
-
if GOOGLE_LIBS_AVAILABLE and GENAI_MODEL_ACCESS_CONFIGURED:
|
| 479 |
with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
|
| 480 |
gr.HTML(alpha_header_html_v3)
|
| 481 |
with gr.Column(elem_classes=["main-content-panel-alpha"]):
|
|
@@ -484,7 +349,9 @@ if GOOGLE_LIBS_AVAILABLE and GENAI_MODEL_ACCESS_CONFIGURED:
|
|
| 484 |
text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
|
| 485 |
use_file_input_cb.change(fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
|
| 486 |
speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
|
| 487 |
-
|
|
|
|
|
|
|
| 488 |
temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
|
| 489 |
gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایینتر = یکنواختی بیشتر.</p>")
|
| 490 |
generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
|
|
@@ -500,9 +367,9 @@ if __name__ == "__main__":
|
|
| 500 |
else:
|
| 501 |
msg = "خطای ناشناخته در شروع برنامه."
|
| 502 |
if not GOOGLE_LIBS_AVAILABLE: msg = "کتابخانههای گوگل بارگذاری نشدند."
|
| 503 |
-
elif not GENAI_MODEL_ACCESS_CONFIGURED: msg = "تنظیمات مدل API جدید (GenerativeModel/configure) یافت نشد."
|
| 504 |
elif NUM_API_KEYS == 0: msg = "هیچ کلید API یافت نشد."
|
| 505 |
_log(f"🔴 برنامه به دلیل '{msg}' اجرا نشد.")
|
| 506 |
with gr.Blocks(title="خطا") as error_demo:
|
| 507 |
-
gr.Markdown(f"# خطای اجرای برنامه\n\n**دلیل:** {msg}\n\nلطفاً لاگهای برنامه یا تنظیمات Space را بررسی کنید.")
|
| 508 |
error_demo.launch()
|
|
|
|
| 8 |
import zipfile
|
| 9 |
import importlib.metadata
|
| 10 |
|
| 11 |
+
# --- START: Import کتابخانههای گوگل با بررسی دقیقتر ---
|
| 12 |
GOOGLE_LIBS_AVAILABLE = False
|
| 13 |
+
GENAI_MODEL_ACCESS_CONFIGURED = False
|
| 14 |
|
| 15 |
+
def _log_startup(message):
|
| 16 |
print(f"[Startup Log] {message}")
|
| 17 |
|
| 18 |
try:
|
|
|
|
| 24 |
except importlib.metadata.PackageNotFoundError:
|
| 25 |
_log_startup("هشدار: پکیج 'google-generativeai' نصب شده، اما نسخهی آن قابل تشخیص نیست.")
|
| 26 |
|
|
|
|
| 27 |
if hasattr(genai, 'GenerativeModel') and hasattr(genai, 'configure'):
|
| 28 |
_log_startup("ویژگیهای 'GenerativeModel' و 'configure' در ماژول 'genai' یافت شدند.")
|
| 29 |
+
GENAI_MODEL_ACCESS_CONFIGURED = True
|
| 30 |
else:
|
| 31 |
+
_log_startup("⛔️ خطای مهم: 'GenerativeModel' یا 'configure' در 'genai' یافت نشد.")
|
| 32 |
|
| 33 |
+
from google.generativeai import types
|
| 34 |
from google.api_core import exceptions as google_exceptions
|
| 35 |
_log_startup("'types' و 'google_exceptions' با موفقیت وارد شدند.")
|
| 36 |
GOOGLE_LIBS_AVAILABLE = True
|
|
|
|
| 41 |
_log_startup(f"❌ خطای ناشناخته در حین import یا بررسی کتابخانههای گوگل: {e_other}")
|
| 42 |
# --- END: Import کتابخانههای گوگل ---
|
| 43 |
|
| 44 |
+
try:
|
| 45 |
+
from pydub import AudioSegment
|
| 46 |
+
PYDUB_AVAILABLE = True
|
| 47 |
+
except ImportError:
|
| 48 |
+
_log_startup("⚠️ کتابخانه pydub یافت نشد. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
| 49 |
+
PYDUB_AVAILABLE = False
|
| 50 |
+
|
| 51 |
+
# --- START: منطق چرخش API Key ---
|
| 52 |
GEMINI_API_KEYS = []
|
| 53 |
i = 1
|
| 54 |
while os.environ.get(f"GEMINI_API_KEY_{i}"):
|
|
|
|
| 56 |
i += 1
|
| 57 |
NUM_API_KEYS = len(GEMINI_API_KEYS)
|
| 58 |
CURRENT_KEY_INDEX_GLOBAL = 0
|
| 59 |
+
def _log(message):
|
| 60 |
print(f"[لاگ آلفا TTS] {message}")
|
| 61 |
if not GOOGLE_LIBS_AVAILABLE:
|
| 62 |
_log("🔴 به دلیل عدم بارگذاری کتابخانههای اصلی گوگل، عملکرد برنامه مختل خواهد شد.")
|
| 63 |
if NUM_API_KEYS == 0:
|
| 64 |
+
_log("⛔️ هشدار: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!")
|
| 65 |
else:
|
| 66 |
_log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
|
| 67 |
def get_api_key_for_attempt(attempt_within_request):
|
|
|
|
| 74 |
global CURRENT_KEY_INDEX_GLOBAL
|
| 75 |
if NUM_API_KEYS > 0: CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
|
| 76 |
# --- END: منطق چرخش API Key ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
SPEAKER_VOICES = [
|
| 79 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
|
|
|
| 82 |
"Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
|
| 83 |
"Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
| 84 |
]
|
| 85 |
+
# ** نام مدل برای TTS با API جدید **
|
| 86 |
+
# این باید نام مدلی باشد که از TTS با API جدید پشتیبانی میکند.
|
| 87 |
+
# "gemini-1.5-flash-latest" یک مدل پایه است. برای TTS، ممکن است نیاز به prompt خاص یا تنظیمات خاص باشد.
|
| 88 |
+
# اگر گوگل مدل خاصی برای TTS معرفی کرده (مثلا "models/text-to-speech")، باید از آن استفاده شود.
|
| 89 |
+
MODEL_NAME_FOR_TTS = "gemini-1.5-flash-latest"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
DEFAULT_MAX_CHUNK_SIZE = 3800
|
| 92 |
DEFAULT_SLEEP_BETWEEN_REQUESTS = 6
|
| 93 |
RETRY_SLEEP_AFTER_QUOTA_ERROR = 2
|
| 94 |
DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
|
| 95 |
|
|
|
|
| 96 |
def save_binary_file(file_name, data):
|
| 97 |
try:
|
| 98 |
with open(file_name, "wb") as f: f.write(data)
|
|
|
|
| 100 |
except Exception as e:
|
| 101 |
_log(f"❌ خطا در ذخیره فایل {file_name}: {e}")
|
| 102 |
return None
|
| 103 |
+
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes: # این تابع ممکن است دیگر لازم نباشد اگر API مستقیما WAV بدهد
|
| 104 |
parameters = parse_audio_mime_type(mime_type)
|
| 105 |
bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
|
| 106 |
num_channels, data_size = 1, len(audio_data)
|
|
|
|
| 115 |
if param_lower_startswith(param, "rate="):
|
| 116 |
try: rate = int(param.split("=", 1)[1])
|
| 117 |
except: pass
|
| 118 |
+
elif param.startswith("audio/L"): # این فرمت احتمالا دیگر استفاده نمیشود
|
| 119 |
try: bits = int(param.split("L", 1)[1])
|
| 120 |
except: pass
|
| 121 |
return {"bits_per_sample": bits, "rate": rate}
|
|
|
|
| 146 |
combined.export(output_path, format="wav"); return True
|
| 147 |
except Exception as e: _log(f"❌ خطا در ادغام: {e}"); return False
|
| 148 |
|
| 149 |
+
def core_generate_audio(text_input, speech_prompt, selected_voice, temperature_val): # پارامتر speech_prompt
|
|
|
|
| 150 |
if not GOOGLE_LIBS_AVAILABLE or not GENAI_MODEL_ACCESS_CONFIGURED:
|
| 151 |
_log("❌ کتابخانههای گوگل یا تنظیمات مدل به درستی بارگذاری نشدهاند.")
|
| 152 |
return None
|
|
|
|
| 158 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
| 159 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
| 160 |
|
| 161 |
+
model_name_to_use = MODEL_NAME_FOR_TTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
if not text_input or not text_input.strip():
|
| 164 |
_log("❌ متن ورودی خالی است."); advance_global_key_index_for_next_request(); return None
|
|
|
|
| 179 |
_log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
|
| 180 |
|
| 181 |
try:
|
|
|
|
| 182 |
genai.configure(api_key=selected_api_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
model_instance = genai.GenerativeModel(model_name_to_use)
|
| 184 |
|
| 185 |
+
# ترکیب speech_prompt با متن اصلی
|
| 186 |
+
final_text_for_api = chunk_text.strip()
|
| 187 |
+
if speech_prompt and speech_prompt.strip(): # استفاده از speech_prompt
|
| 188 |
+
processed_prompt = speech_prompt.strip()
|
| 189 |
if not re.search(r'[.!?؟،:۔]$', processed_prompt): processed_prompt += "،"
|
| 190 |
+
final_text_for_api = f"{processed_prompt} {final_text_for_api}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
+
# ** نحوه کنترل صدا (selected_voice) با API جدید نیاز به بررسی دارد **
|
| 193 |
+
# در حال حاضر selected_voice مستقیماً استفاده نمیشود. ممکن است نیاز به prompt engineering
|
| 194 |
+
# یا پارامترهای خاص در generation_config باشد اگر مدل از آن پشتیبانی کند.
|
| 195 |
+
# مثال: final_text_for_api = f"با صدای {selected_voice}، {final_text_for_api}" (این فقط یک حدس است)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
+
_log(f" متن نهایی برای API: '{final_text_for_api[:100]}...'")
|
|
|
|
|
|
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
response = model_instance.generate_content(
|
| 200 |
+
contents=final_text_for_api,
|
| 201 |
+
generation_config=types.GenerationConfig(
|
| 202 |
temperature=temperature_val,
|
| 203 |
+
response_mime_type="audio/wav"
|
|
|
|
| 204 |
)
|
|
|
|
|
|
|
|
|
|
| 205 |
)
|
|
|
|
|
|
|
| 206 |
|
|
|
|
|
|
|
|
|
|
| 207 |
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
|
| 208 |
inline_data = response.candidates[0].content.parts[0].inline_data
|
| 209 |
data_buffer = inline_data.data
|
| 210 |
+
|
| 211 |
+
# mime_type از پاسخ برای اطمینان (اگرچه wav درخواست کردهایم)
|
| 212 |
+
mime_type_from_response = inline_data.mime_type
|
| 213 |
+
_log(f" نوع MIME دریافت شده از API: {mime_type_from_response}")
|
| 214 |
+
|
| 215 |
+
ext = mimetypes.guess_extension(mime_type_from_response) or ".wav"
|
|
|
|
| 216 |
if not ext.startswith("."): ext = "." + ext
|
| 217 |
|
| 218 |
+
# تابع convert_to_wav ممکن است دیگر لازم نباشد اگر API مستقیم فرمت استاندارد بدهد.
|
| 219 |
+
# if "audio/L" in mime_type_from_response and ext == ".wav":
|
| 220 |
+
# data_buffer = convert_to_wav(data_buffer, mime_type_from_response)
|
| 221 |
+
|
| 222 |
fname_base = f"{output_base_name}_part{chunk_idx+1:03d}"
|
| 223 |
temp_fpath_for_chunk = f"{fname_base}{ext}"
|
| 224 |
if os.path.exists(temp_fpath_for_chunk):
|
|
|
|
| 232 |
if chunk_idx < len(text_chunks) - 1: time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
|
| 233 |
break
|
| 234 |
else:
|
| 235 |
+
error_message = "پاسخ API بدون داده صوتی معتبر."
|
| 236 |
+
if hasattr(response, 'prompt_feedback') and response.prompt_feedback:
|
| 237 |
+
error_message += f" بازخورد Prompt: {response.prompt_feedback}"
|
| 238 |
+
_log(f" ⚠️ {error_message} با کلید {key_display_num}. پاسخ کلی: {str(response)[:200]}")
|
| 239 |
|
| 240 |
except google_exceptions.ResourceExhausted as e_quota:
|
| 241 |
_log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
|
|
|
|
| 245 |
|
| 246 |
except Exception as e_general:
|
| 247 |
error_type_name = type(e_general).__name__
|
| 248 |
+
error_msg_str = str(e_general)
|
| 249 |
+
_log(f" ❌ خطای عمومی ({error_type_name}) در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {error_msg_str[:200]}")
|
| 250 |
+
if "response_mime_type" in error_msg_str.lower() or "modality" in error_msg_str.lower() or "audio" in error_msg_str.lower() :
|
| 251 |
+
_log(f" این خطا ممکن است مربوط به عدم پشتیبانی مدل '{model_name_to_use}' از خروجی صوتی یا تنظیمات نادرست response_mime_type باشد.")
|
| 252 |
+
if "model" in error_msg_str.lower() and "not found" in error_msg_str.lower():
|
| 253 |
_log(f" مدل '{model_name_to_use}' یافت نشد یا برای این کلید API در دسترس نیست.")
|
| 254 |
+
if "permission denied" in error_msg_str.lower() or "access denied" in error_msg_str.lower():
|
| 255 |
+
_log(f" خطای دسترسی با کلید API شماره {key_display_num}. ممکن است کلید نامعتبر باشد یا به مدل دسترسی نداشته باشد.")
|
| 256 |
+
|
| 257 |
|
| 258 |
if attempt_num_for_chunk < max_attempts_for_chunk - 1: time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
|
| 259 |
else: all_chunks_processed = False
|
|
|
|
| 264 |
_log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود."); all_chunks_processed = False; break
|
| 265 |
|
| 266 |
advance_global_key_index_for_next_request()
|
| 267 |
+
|
| 268 |
if not all_chunks_processed or not generated_files:
|
| 269 |
_log("❌ هیچ فایل صوتی معتبری تولید نشد.")
|
| 270 |
+
for fp_cleanup in generated_files:
|
| 271 |
try: os.remove(fp_cleanup)
|
| 272 |
except: pass
|
| 273 |
return None
|
|
|
|
| 284 |
if os.path.exists(renamed_first_chunk): os.remove(renamed_first_chunk)
|
| 285 |
os.rename(generated_files[0], renamed_first_chunk); final_audio_file = renamed_first_chunk
|
| 286 |
except Exception as e_rename: _log(f"خطا در تغییر نام اولین قطعه: {e_rename}"); final_audio_file = generated_files[0]
|
| 287 |
+
for fp_cleanup_merge in generated_files:
|
| 288 |
if final_audio_file and os.path.abspath(fp_cleanup_merge) == os.path.abspath(final_audio_file): continue
|
| 289 |
try: os.remove(fp_cleanup_merge)
|
| 290 |
except: pass
|
|
|
|
| 310 |
else: _log(f"❓ وضعیت نامشخص برای فایل نهایی."); return None
|
| 311 |
return final_audio_file
|
| 312 |
|
|
|
|
|
|
|
| 313 |
def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
|
| 314 |
actual_text = ""
|
| 315 |
if use_file_input:
|
|
|
|
| 322 |
else:
|
| 323 |
actual_text = text_to_speak
|
| 324 |
if not actual_text or not actual_text.strip(): _log("❌ متن ورودی خالی."); return None
|
| 325 |
+
|
| 326 |
+
if not GOOGLE_LIBS_AVAILABLE or not GENAI_MODEL_ACCESS_CONFIGURED:
|
| 327 |
gr.Warning("خطای سیستمی: کتابخانههای مورد نیاز یا تنظیمات مدل به درستی بارگذاری نشدهاند.")
|
| 328 |
return None
|
| 329 |
if NUM_API_KEYS == 0:
|
| 330 |
gr.Warning("خطای سیستمی: کلید API موجود نیست.")
|
| 331 |
return None
|
| 332 |
+
|
| 333 |
+
final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature) # ارسال speech_prompt
|
| 334 |
+
|
| 335 |
if final_path is None:
|
| 336 |
+
gr.Info("امکان تولید صدا وجود ندارد. لطفاً دقایقی دیگر، با متن کوتاهتری یا با بررسی لاگها برای خطاهای احتمالی API، مجدداً تلاش کنید.")
|
| 337 |
return final_path
|
| 338 |
|
| 339 |
+
# --- CSS و UI (بدون تغییر) ---
|
| 340 |
custom_css_inspired_by_image = f"""@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');:root {{ --app-font: 'Vazirmatn', sans-serif; --app-header-grad-start: #2980b9; --app-header-grad-end: #2ecc71; --app-panel-bg: #FFFFFF; --app-input-bg: #F7F7F7; --app-button-bg: #2979FF; --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); --app-text-primary: #333; --app-text-secondary: #555; --app-border-color: #E0E0E0; --radius-card: 20px; --radius-input: 8px; --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1); --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);}}body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 16px; line-height: 1.65; }}.gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}.app-header-alpha {{ padding: 3rem 1.5rem 4rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2); }}.app-header-alpha h1 {{ font-size: 2.4em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }}.app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}.main-content-panel-alpha {{ padding: 1.8rem 1.5rem; max-width: 680px; margin: -2.5rem auto 2rem auto; width: 90%; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); position:relative; z-index:10; }}@media (max-width: 768px) {{ .main-content-panel-alpha {{ width: 95%; padding: 1.5rem 1rem; margin-top: -2rem; }} .app-header-alpha h1 {{font-size:2em;}} .app-header-alpha p {{font-size:1em;}} }}footer {{display:none !important;}}.gr-button.generate-button-final {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.8rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.3s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.5rem !important; }}.gr-button.generate-button-final:hover {{ filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba(41,121,255,0.6);}}.gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-file > label + div {{ border-radius: var(--radius-input) !important; border: 1px solid var(--app-border-color) !important; background-color: var(--app-input-bg) !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); padding: 0.75rem !important; }}.gr-file > label + div {{ text-align:center; border-style: dashed !important; }}.gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-textbox > label + div > textarea:focus {{ border-color: var(--app-button-bg) !important; box-shadow: 0 0 0 3px rgba(41,121,255,0.2) !important; }}label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}.section-title-main-alpha {{ font-size: 1.1em; color: var(--app-text-secondary); margin-bottom:1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--app-border-color); font-weight:500; text-align:right; }}label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}#output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}.temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}.app-footer-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.8; margin-top:3rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}"""
|
| 341 |
alpha_header_html_v3 = """<div class='app-header-alpha'><h1>Alpha TTS</h1><p>جادوی تبدیل متن به صدا در دستان شما</p></div>"""
|
| 342 |
|
| 343 |
+
if GOOGLE_LIBS_AVAILABLE and GENAI_MODEL_ACCESS_CONFIGURED: # بررسی هر دو پرچم
|
| 344 |
with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
|
| 345 |
gr.HTML(alpha_header_html_v3)
|
| 346 |
with gr.Column(elem_classes=["main-content-panel-alpha"]):
|
|
|
|
| 349 |
text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
|
| 350 |
use_file_input_cb.change(fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
|
| 351 |
speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
|
| 352 |
+
# speaker_voice_dd دیگر مستقیماً به API ارسال نمیشود، اما برای کاربر باقی میماند.
|
| 353 |
+
# برای کنترل صدا، باید از روشهای دیگر (مانند prompt engineering) استفاده کرد.
|
| 354 |
+
speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده (توجه: این گزینه فعلاً تأثیر مستقیم روی صدای API جدید ندارد)", value="Charon", elem_id="speaker_voice_alpha_v3")
|
| 355 |
temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
|
| 356 |
gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایینتر = یکنواختی بیشتر.</p>")
|
| 357 |
generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
|
|
|
|
| 367 |
else:
|
| 368 |
msg = "خطای ناشناخته در شروع برنامه."
|
| 369 |
if not GOOGLE_LIBS_AVAILABLE: msg = "کتابخانههای گوگل بارگذاری نشدند."
|
| 370 |
+
elif not GENAI_MODEL_ACCESS_CONFIGURED: msg = "تنظیمات مدل API جدید (GenerativeModel/configure) یافت نشد یا سازگار نیست."
|
| 371 |
elif NUM_API_KEYS == 0: msg = "هیچ کلید API یافت نشد."
|
| 372 |
_log(f"🔴 برنامه به دلیل '{msg}' اجرا نشد.")
|
| 373 |
with gr.Blocks(title="خطا") as error_demo:
|
| 374 |
+
gr.Markdown(f"# خطای اجرای برنامه\n\n**دلیل:** {msg}\n\nلطفاً لاگهای برنامه یا تنظیمات Space را بررسی کنید و از صحت نسخه کتابخانه google-generativeai و نام مدل TTS اطمینان حاصل کنید.")
|
| 375 |
error_demo.launch()
|