Update app.py
Browse files
app.py
CHANGED
|
@@ -6,56 +6,43 @@ import re
|
|
| 6 |
import struct
|
| 7 |
import time
|
| 8 |
import zipfile
|
| 9 |
-
import importlib.metadata
|
| 10 |
|
| 11 |
-
# --- START: Import کتابخانههای گوگل
|
| 12 |
GOOGLE_LIBS_AVAILABLE = False
|
| 13 |
-
|
| 14 |
|
| 15 |
-
def _log_startup(message):
|
| 16 |
print(f"[Startup Log] {message}")
|
| 17 |
|
| 18 |
try:
|
| 19 |
-
# ابتدا سعی در import کردن google.generativeai
|
| 20 |
import google.generativeai as genai
|
| 21 |
_log_startup("ماژول 'google.generativeai' با موفقیت به عنوان 'genai' وارد شد.")
|
| 22 |
-
|
| 23 |
-
# بررسی نسخه نصب شده
|
| 24 |
try:
|
| 25 |
version = importlib.metadata.version('google-generativeai')
|
| 26 |
_log_startup(f"نسخه نصب شده 'google-generativeai': {version}")
|
| 27 |
except importlib.metadata.PackageNotFoundError:
|
| 28 |
_log_startup("هشدار: پکیج 'google-generativeai' نصب شده، اما نسخهی آن قابل تشخیص نیست.")
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
if hasattr(genai, '
|
| 32 |
-
_log_startup("ویژگی '
|
| 33 |
-
|
| 34 |
else:
|
| 35 |
-
_log_startup("⛔️ خطای مهم:
|
| 36 |
-
_log_startup(f" محتویات ماژول genai: {dir(genai)}") # نمایش تمام محتویات برای دیباگ بیشتر
|
| 37 |
|
| 38 |
-
|
| 39 |
-
from google.generativeai import types
|
| 40 |
from google.api_core import exceptions as google_exceptions
|
| 41 |
_log_startup("'types' و 'google_exceptions' با موفقیت وارد شدند.")
|
| 42 |
GOOGLE_LIBS_AVAILABLE = True
|
| 43 |
|
| 44 |
except ImportError as e:
|
| 45 |
_log_startup(f"❌ خطای حیاتی در Import: {e}")
|
| 46 |
-
_log_startup(" لطفاً از صحت 'google-generativeai' و 'google-api-core' در requirements.txt و ریاستارت کامل Space مطمئن شوید.")
|
| 47 |
except Exception as e_other:
|
| 48 |
_log_startup(f"❌ خطای ناشناخته در حین import یا بررسی کتابخانههای گوگل: {e_other}")
|
| 49 |
-
|
| 50 |
# --- END: Import کتابخانههای گوگل ---
|
| 51 |
|
| 52 |
-
|
| 53 |
-
from pydub import AudioSegment
|
| 54 |
-
PYDUB_AVAILABLE = True
|
| 55 |
-
except ImportError:
|
| 56 |
-
_log_startup("⚠️ کتابخانه pydub یافت نشد. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
| 57 |
-
PYDUB_AVAILABLE = False
|
| 58 |
-
|
| 59 |
# --- START: منطق چرخش API Key (بدون تغییر نسبت به قبل) ---
|
| 60 |
GEMINI_API_KEYS = []
|
| 61 |
i = 1
|
|
@@ -82,26 +69,13 @@ def advance_global_key_index_for_next_request():
|
|
| 82 |
global CURRENT_KEY_INDEX_GLOBAL
|
| 83 |
if NUM_API_KEYS > 0: CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
|
| 84 |
# --- END: منطق چرخش API Key ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
# ... (بقیه کد شامل SPEAKER_VOICES, FIXED_MODEL_NAME, توابع save_binary_file, convert_to_wav, etc. بدون تغییر باقی میماند) ...
|
| 87 |
-
# ... فقط مطمئن شوید که در core_generate_audio، قبل از استفاده از genai.Client، چک GENAI_CLIENT_AVAILABLE را اضافه کنید ...
|
| 88 |
-
|
| 89 |
-
# تابع core_generate_audio با یک بررسی اضافه در ابتدا
|
| 90 |
-
def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
|
| 91 |
-
if not GOOGLE_LIBS_AVAILABLE or not GENAI_CLIENT_AVAILABLE:
|
| 92 |
-
_log("❌ کتابخانههای گوگل یا genai.Client به درستی بارگذاری نشدهاند. امکان تولید صدا وجود ندارد.")
|
| 93 |
-
return None
|
| 94 |
-
# ... (بقیه کد core_generate_audio که قبلاً داشتید، از اینجا شروع میشود) ...
|
| 95 |
-
# ... و به جای if not GOOGLE_LIBS_AVAILABLE: در ابتدای تابع قبلی، حالا از GENAI_CLIENT_AVAILABLE هم چک میکنید ...
|
| 96 |
-
|
| 97 |
-
_log("🚀 شروع فرآیند تولید صدا...")
|
| 98 |
-
# ... (بقیه کد core_generate_audio بدون تغییر)
|
| 99 |
-
# client = genai.Client(api_key=selected_api_key) # این خط باید بعد از بررسی GENAI_CLIENT_AVAILABLE باشد
|
| 100 |
-
|
| 101 |
-
# ... (بقیه کد شامل SPEAKER_VOICES, FIXED_MODEL_NAME, توابع save_binary_file, convert_to_wav, etc. باید از نسخه کامل قبلی کپی شود)
|
| 102 |
-
# این یک خلاصه است، کد کامل قبلی را با تغییرات بخش import ترکیب کنید.
|
| 103 |
-
|
| 104 |
-
# --- کد کامل توابع کمکی (برای اطمینان از کامل بودن) ---
|
| 105 |
SPEAKER_VOICES = [
|
| 106 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
| 107 |
"Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
|
|
@@ -109,12 +83,39 @@ SPEAKER_VOICES = [
|
|
| 109 |
"Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
|
| 110 |
"Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
| 111 |
]
|
| 112 |
-
FIXED_MODEL_NAME = "gemini-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
def save_binary_file(file_name, data):
|
| 119 |
try:
|
| 120 |
with open(file_name, "wb") as f: f.write(data)
|
|
@@ -122,7 +123,6 @@ def save_binary_file(file_name, data):
|
|
| 122 |
except Exception as e:
|
| 123 |
_log(f"❌ خطا در ذخیره فایل {file_name}: {e}")
|
| 124 |
return None
|
| 125 |
-
|
| 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"]
|
|
@@ -131,23 +131,20 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
|
| 131 |
byte_rate, chunk_size = rate * block_align, 36 + data_size
|
| 132 |
header = struct.pack("<4sI4s4sIHHIIHH4sI", b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels, rate, byte_rate, block_align, bits_per_sample, b"data", data_size)
|
| 133 |
return header + audio_data
|
| 134 |
-
|
| 135 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
|
| 136 |
-
bits, rate = 16, 24000
|
| 137 |
for param in mime_type.split(";"):
|
| 138 |
param = param.strip()
|
| 139 |
-
if
|
| 140 |
try: rate = int(param.split("=", 1)[1])
|
| 141 |
except: pass
|
| 142 |
elif param.startswith("audio/L"):
|
| 143 |
try: bits = int(param.split("L", 1)[1])
|
| 144 |
except: pass
|
| 145 |
return {"bits_per_sample": bits, "rate": rate}
|
| 146 |
-
|
| 147 |
def smart_text_split(text, max_size=3800):
|
| 148 |
if len(text) <= max_size: return [text]
|
| 149 |
-
chunks, current_chunk = [], ""
|
| 150 |
-
sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
|
| 151 |
for sentence in sentences:
|
| 152 |
if len(current_chunk) + len(sentence) + 1 > max_size:
|
| 153 |
if current_chunk: chunks.append(current_chunk.strip())
|
|
@@ -155,63 +152,53 @@ def smart_text_split(text, max_size=3800):
|
|
| 155 |
while len(current_chunk) > max_size:
|
| 156 |
split_idx = -1
|
| 157 |
for char_to_find in ['،', ',', ';', ':', ' ']:
|
| 158 |
-
try:
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
except ValueError:
|
| 162 |
-
continue
|
| 163 |
-
if split_idx != -1:
|
| 164 |
-
part = current_chunk[:split_idx+1]
|
| 165 |
-
current_chunk = current_chunk[split_idx+1:]
|
| 166 |
-
else:
|
| 167 |
-
part = current_chunk[:max_size]
|
| 168 |
-
current_chunk = current_chunk[max_size:]
|
| 169 |
chunks.append(part.strip())
|
| 170 |
-
else:
|
| 171 |
-
if current_chunk:
|
| 172 |
-
current_chunk += " " + sentence
|
| 173 |
-
else:
|
| 174 |
-
current_chunk = sentence
|
| 175 |
if current_chunk: chunks.append(current_chunk.strip())
|
| 176 |
-
|
| 177 |
-
return final_chunks
|
| 178 |
-
|
| 179 |
def merge_audio_files_func(file_paths, output_path):
|
| 180 |
-
if not PYDUB_AVAILABLE: _log("⚠️ pydub
|
| 181 |
try:
|
| 182 |
combined = AudioSegment.empty()
|
| 183 |
for i, fp in enumerate(file_paths):
|
| 184 |
if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
|
| 185 |
-
else: _log(f"⚠️ فایل
|
| 186 |
-
combined.export(output_path, format="wav")
|
| 187 |
-
|
| 188 |
-
|
| 189 |
|
| 190 |
def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
|
| 191 |
-
if not GOOGLE_LIBS_AVAILABLE
|
| 192 |
-
_log("❌ کتابخانههای گوگل به
|
| 193 |
-
return None
|
| 194 |
-
if not GENAI_CLIENT_AVAILABLE: # بررسی وجود Client
|
| 195 |
-
_log("❌ ویژگی 'Client' در کتابخانه 'google.generativeai' یافت نشد. لطفاً نسخه کتابخانه را بررسی کنید.")
|
| 196 |
return None
|
| 197 |
if NUM_API_KEYS == 0:
|
| 198 |
-
_log("❌ هیچ کلید API
|
| 199 |
return None
|
| 200 |
|
| 201 |
-
_log("🚀 شروع فرآیند تولید صدا...")
|
| 202 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
| 203 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
if not text_input or not text_input.strip():
|
| 206 |
-
_log("❌ متن ورودی خالی است.")
|
| 207 |
-
advance_global_key_index_for_next_request()
|
| 208 |
-
return None
|
| 209 |
-
|
| 210 |
text_chunks = smart_text_split(text_input, max_chunk)
|
| 211 |
if not text_chunks:
|
| 212 |
-
_log("❌ متن قابل پردازش
|
| 213 |
-
advance_global_key_index_for_next_request()
|
| 214 |
-
return None
|
| 215 |
|
| 216 |
generated_files = []
|
| 217 |
all_chunks_processed = True
|
|
@@ -222,58 +209,203 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 222 |
max_attempts_for_chunk = NUM_API_KEYS
|
| 223 |
|
| 224 |
for attempt_num_for_chunk in range(max_attempts_for_chunk):
|
| 225 |
-
selected_api_key, key_display_num,
|
| 226 |
-
_log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} ب
|
| 227 |
|
| 228 |
try:
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
if prompt_input and prompt_input.strip():
|
| 231 |
processed_prompt = prompt_input.strip()
|
| 232 |
if not re.search(r'[.!?؟،:۔]$', processed_prompt): processed_prompt += "،"
|
| 233 |
final_text_for_api = f"{processed_prompt} {chunk_text.strip()}"
|
| 234 |
else: final_text_for_api = chunk_text.strip()
|
| 235 |
|
| 236 |
-
|
| 237 |
-
config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
|
| 239 |
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
|
| 245 |
-
inline_data = response.candidates[0].content.parts[0].inline_data
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
if not ext.startswith("."): ext = "." + ext
|
|
|
|
|
|
|
| 249 |
temp_fpath_for_chunk = f"{fname_base}{ext}"
|
| 250 |
if os.path.exists(temp_fpath_for_chunk):
|
| 251 |
try: os.remove(temp_fpath_for_chunk)
|
| 252 |
except OSError: pass
|
|
|
|
| 253 |
fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
|
| 254 |
if fpath:
|
| 255 |
generated_files.append(fpath); chunk_processed_successfully = True
|
| 256 |
_log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
|
| 257 |
if chunk_idx < len(text_chunks) - 1: time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
|
| 258 |
break
|
| 259 |
-
else:
|
|
|
|
|
|
|
| 260 |
except google_exceptions.ResourceExhausted as e_quota:
|
| 261 |
_log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
|
| 262 |
if attempt_num_for_chunk < max_attempts_for_chunk - 1:
|
| 263 |
_log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه."); time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
|
| 264 |
else: _log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند (خطای سهمیه)."); all_chunks_processed = False
|
|
|
|
| 265 |
except Exception as e_general:
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
if attempt_num_for_chunk < max_attempts_for_chunk - 1: time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
|
| 268 |
else: all_chunks_processed = False
|
|
|
|
| 269 |
if chunk_processed_successfully: break
|
|
|
|
| 270 |
if not chunk_processed_successfully:
|
| 271 |
_log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود."); all_chunks_processed = False; break
|
|
|
|
| 272 |
advance_global_key_index_for_next_request()
|
|
|
|
| 273 |
if not all_chunks_processed or not generated_files:
|
| 274 |
_log("❌ هیچ فایل صوتی معتبری تولید نشد.")
|
| 275 |
-
for
|
| 276 |
-
try: os.remove(
|
| 277 |
except: pass
|
| 278 |
return None
|
| 279 |
final_audio_file = None; final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
|
|
@@ -289,9 +421,9 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 289 |
if os.path.exists(renamed_first_chunk): os.remove(renamed_first_chunk)
|
| 290 |
os.rename(generated_files[0], renamed_first_chunk); final_audio_file = renamed_first_chunk
|
| 291 |
except Exception as e_rename: _log(f"خطا در تغییر نام اولین قطعه: {e_rename}"); final_audio_file = generated_files[0]
|
| 292 |
-
for
|
| 293 |
-
if final_audio_file and os.path.abspath(
|
| 294 |
-
try: os.remove(
|
| 295 |
except: pass
|
| 296 |
else:
|
| 297 |
_log("⚠️ pydub نیست. اولین قطعه ارائه میشود.")
|
|
@@ -303,18 +435,20 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
|
|
| 303 |
for i_gf in range(1, len(generated_files)):
|
| 304 |
try: os.remove(generated_files[i_gf])
|
| 305 |
except: pass
|
| 306 |
-
except Exception as
|
| 307 |
elif len(generated_files) == 1:
|
| 308 |
try:
|
| 309 |
target_ext = os.path.splitext(generated_files[0])[1]; final_single_fn = f"{final_output_path_base}{target_ext}"
|
| 310 |
if os.path.exists(final_single_fn): os.remove(final_single_fn)
|
| 311 |
os.rename(generated_files[0], final_single_fn); final_audio_file = final_single_fn
|
| 312 |
-
except Exception as
|
| 313 |
if final_audio_file and os.path.exists(final_audio_file): _log(f"✅ فایل نهایی: {os.path.basename(final_audio_file)}")
|
| 314 |
elif final_audio_file: _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!"); return None
|
| 315 |
else: _log(f"❓ وضعیت نامشخص برای فایل نهایی."); return None
|
| 316 |
return final_audio_file
|
| 317 |
|
|
|
|
|
|
|
| 318 |
def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
|
| 319 |
actual_text = ""
|
| 320 |
if use_file_input:
|
|
@@ -327,47 +461,21 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
|
|
| 327 |
else:
|
| 328 |
actual_text = text_to_speak
|
| 329 |
if not actual_text or not actual_text.strip(): _log("❌ متن ورودی خالی."); return None
|
| 330 |
-
if not GOOGLE_LIBS_AVAILABLE or not
|
| 331 |
-
gr.Warning("خطای سیستمی: کتابخانههای مورد نیاز بارگذاری نشدهاند.")
|
| 332 |
return None
|
| 333 |
if NUM_API_KEYS == 0:
|
| 334 |
gr.Warning("خطای سیستمی: کلید API موجود نیست.")
|
| 335 |
return None
|
| 336 |
-
final_path = core_generate_audio(actual_text,
|
| 337 |
if final_path is None:
|
| 338 |
gr.Info("امکان تولید صدا وجود ندارد. لطفاً دقایقی دیگر یا با متن کوتاهتری امتحان کنید.")
|
| 339 |
return final_path
|
| 340 |
|
| 341 |
-
# ---
|
| 342 |
-
custom_css_inspired_by_image = f"""
|
| 343 |
-
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
|
| 344 |
-
: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);}}
|
| 345 |
-
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; }}
|
| 346 |
-
.gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
|
| 347 |
-
.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); }}
|
| 348 |
-
.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); }}
|
| 349 |
-
.app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}
|
| 350 |
-
.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; }}
|
| 351 |
-
@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;}} }}
|
| 352 |
-
footer {{display:none !important;}}
|
| 353 |
-
.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; }}
|
| 354 |
-
.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);}}
|
| 355 |
-
.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; }}
|
| 356 |
-
.gr-file > label + div {{ text-align:center; border-style: dashed !important; }}
|
| 357 |
-
.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; }}
|
| 358 |
-
label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}
|
| 359 |
-
.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; }}
|
| 360 |
-
label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}
|
| 361 |
-
label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}
|
| 362 |
-
label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
|
| 363 |
-
label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
|
| 364 |
-
label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
|
| 365 |
-
#output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
|
| 366 |
-
.temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
|
| 367 |
-
.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);}}"""
|
| 368 |
alpha_header_html_v3 = """<div class='app-header-alpha'><h1>Alpha TTS</h1><p>جادوی تبدیل متن به صدا در دستان شما</p></div>"""
|
| 369 |
|
| 370 |
-
if GOOGLE_LIBS_AVAILABLE and
|
| 371 |
with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
|
| 372 |
gr.HTML(alpha_header_html_v3)
|
| 373 |
with gr.Column(elem_classes=["main-content-panel-alpha"]):
|
|
@@ -376,24 +484,24 @@ if GOOGLE_LIBS_AVAILABLE and GENAI_CLIENT_AVAILABLE:
|
|
| 376 |
text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
|
| 377 |
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])
|
| 378 |
speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
|
| 379 |
-
speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3")
|
| 380 |
temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
|
| 381 |
gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایینتر = یکنواختی بیشتر.</p>")
|
| 382 |
generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
|
| 383 |
output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
|
| 384 |
generate_button.click(fn=gradio_tts_interface, inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio])
|
| 385 |
gr.Markdown("<h3 class='section-title-main-alpha' style='margin-top:2.5rem; text-align:center; border-bottom:none;'>نمونههای کاربردی</h3>", elem_id="examples_section_title_v3")
|
| 386 |
-
gr.Examples(examples=[[False,None,"سلام بر شما، امیدوارم روز خوبی داشته باشید.","با لحنی گرم و صمیمی.","Zephyr",0.85],[False,None,"این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.","با صدایی طبیعی و روان.","Charon",0.9],[False,None,"آیا میتوانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.","با کنجکاوی","Puck",0.95],[False,None,"این یک متن بسیار طولانی است
|
| 387 |
gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
|
| 388 |
|
| 389 |
if __name__ == "__main__":
|
| 390 |
-
if GOOGLE_LIBS_AVAILABLE and
|
| 391 |
demo.launch()
|
| 392 |
else:
|
|
|
|
| 393 |
if not GOOGLE_LIBS_AVAILABLE: msg = "کتابخانههای گوگل بارگذاری نشدند."
|
| 394 |
-
elif not
|
| 395 |
elif NUM_API_KEYS == 0: msg = "هیچ کلید API یافت نشد."
|
| 396 |
-
else: msg = "خطای ناشناخته در شروع برنامه."
|
| 397 |
_log(f"🔴 برنامه به دلیل '{msg}' اجرا نشد.")
|
| 398 |
with gr.Blocks(title="خطا") as error_demo:
|
| 399 |
gr.Markdown(f"# خطای اجرای برنامه\n\n**دلیل:** {msg}\n\nلطفاً لاگهای برنامه یا تنظیمات Space را بررسی کنید.")
|
|
|
|
| 6 |
import struct
|
| 7 |
import time
|
| 8 |
import zipfile
|
| 9 |
+
import importlib.metadata
|
| 10 |
|
| 11 |
+
# --- START: Import کتابخانههای گوگل ---
|
| 12 |
GOOGLE_LIBS_AVAILABLE = False
|
| 13 |
+
GENAI_MODEL_ACCESS_CONFIGURED = False # برای اطمینان از اینکه configure قبل از model استفاده میشود
|
| 14 |
|
| 15 |
+
def _log_startup(message):
|
| 16 |
print(f"[Startup Log] {message}")
|
| 17 |
|
| 18 |
try:
|
|
|
|
| 19 |
import google.generativeai as genai
|
| 20 |
_log_startup("ماژول 'google.generativeai' با موفقیت به عنوان 'genai' وارد شد.")
|
|
|
|
|
|
|
| 21 |
try:
|
| 22 |
version = importlib.metadata.version('google-generativeai')
|
| 23 |
_log_startup(f"نسخه نصب شده 'google-generativeai': {version}")
|
| 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 # نشاندهنده آمادگی برای استفاده از API جدید
|
| 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
|
| 38 |
|
| 39 |
except ImportError as e:
|
| 40 |
_log_startup(f"❌ خطای حیاتی در Import: {e}")
|
|
|
|
| 41 |
except Exception as e_other:
|
| 42 |
_log_startup(f"❌ خطای ناشناخته در حین import یا بررسی کتابخانههای گوگل: {e_other}")
|
|
|
|
| 43 |
# --- END: Import کتابخانههای گوگل ---
|
| 44 |
|
| 45 |
+
# ... (بقیه import های pydub و منطق چرخش کلید API و توابع کمکی بدون تغییر باقی میمانند) ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
# --- START: منطق چرخش API Key (بدون تغییر نسبت به قبل) ---
|
| 47 |
GEMINI_API_KEYS = []
|
| 48 |
i = 1
|
|
|
|
| 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",
|
| 81 |
"Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
|
|
|
|
| 83 |
"Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
|
| 84 |
"Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
| 85 |
]
|
| 86 |
+
FIXED_MODEL_NAME = "models/gemini-1.5-flash-latest" # نام مدل برای API جدید ممکن است متفاوت باشد، این را چک کنید. برای TTS باید از مدل مخصوص TTS استفاده کرد.
|
| 87 |
+
# نام مدل صحیح برای TTS در API جدید: "gemini-1.5-flash" و استفاده از response_mime_type="audio/ogg" یا "audio/wav" در generation_config
|
| 88 |
+
# یا استفاده از مدل خاص TTS اگر موجود باشد. فعلا "models/tts-alpha" یا مشابه را در نظر میگیریم
|
| 89 |
+
# بر اساس داکیومنت جدید، مدلهای TTS ممکن است به صورت "models/text-to-speech" یا نامهای خاص دیگر باشند.
|
| 90 |
+
# برای Gemini 1.5 Flash و قابلیت TTS، باید مدل درست را پیدا کنیم.
|
| 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 |
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"]
|
|
|
|
| 131 |
byte_rate, chunk_size = rate * block_align, 36 + data_size
|
| 132 |
header = struct.pack("<4sI4s4sIHHIIHH4sI", b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels, rate, byte_rate, block_align, bits_per_sample, b"data", data_size)
|
| 133 |
return header + audio_data
|
|
|
|
| 134 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
|
| 135 |
+
bits, rate = 16, 24000; param_lower_startswith = lambda p, s: p.lower().startswith(s)
|
| 136 |
for param in mime_type.split(";"):
|
| 137 |
param = param.strip()
|
| 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}
|
|
|
|
| 145 |
def smart_text_split(text, max_size=3800):
|
| 146 |
if len(text) <= max_size: return [text]
|
| 147 |
+
chunks, current_chunk = [], ""; sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
|
|
|
|
| 148 |
for sentence in sentences:
|
| 149 |
if len(current_chunk) + len(sentence) + 1 > max_size:
|
| 150 |
if current_chunk: chunks.append(current_chunk.strip())
|
|
|
|
| 152 |
while len(current_chunk) > max_size:
|
| 153 |
split_idx = -1
|
| 154 |
for char_to_find in ['،', ',', ';', ':', ' ']:
|
| 155 |
+
try: split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size); break
|
| 156 |
+
except ValueError: continue
|
| 157 |
+
part, current_chunk = (current_chunk[:split_idx+1], current_chunk[split_idx+1:]) if split_idx != -1 else (current_chunk[:max_size], current_chunk[max_size:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
chunks.append(part.strip())
|
| 159 |
+
else: current_chunk += (" " if current_chunk else "") + sentence
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
if current_chunk: chunks.append(current_chunk.strip())
|
| 161 |
+
return [c for c in chunks if c]
|
|
|
|
|
|
|
| 162 |
def merge_audio_files_func(file_paths, output_path):
|
| 163 |
+
if not PYDUB_AVAILABLE: _log("⚠️ pydub نیست."); return False
|
| 164 |
try:
|
| 165 |
combined = AudioSegment.empty()
|
| 166 |
for i, fp in enumerate(file_paths):
|
| 167 |
if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
|
| 168 |
+
else: _log(f"⚠️ فایل {fp} نیست.")
|
| 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
|
| 177 |
if NUM_API_KEYS == 0:
|
| 178 |
+
_log("❌ هیچ کلید API موجود نیست.")
|
| 179 |
return None
|
| 180 |
|
| 181 |
+
_log("🚀 شروع فرآیند تولید صدا (با API جدید)...")
|
| 182 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
| 183 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
| 184 |
+
|
| 185 |
+
# ** مهم: نام مدل برای TTS با API جدید باید بررسی شود. **
|
| 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
|
|
|
|
|
|
|
|
|
|
| 199 |
text_chunks = smart_text_split(text_input, max_chunk)
|
| 200 |
if not text_chunks:
|
| 201 |
+
_log("❌ متن قابل پردازش نیست."); advance_global_key_index_for_next_request(); return None
|
|
|
|
|
|
|
| 202 |
|
| 203 |
generated_files = []
|
| 204 |
all_chunks_processed = True
|
|
|
|
| 209 |
max_attempts_for_chunk = NUM_API_KEYS
|
| 210 |
|
| 211 |
for attempt_num_for_chunk in range(max_attempts_for_chunk):
|
| 212 |
+
selected_api_key, key_display_num, _ = get_api_key_for_attempt(attempt_num_for_chunk)
|
| 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 |
if prompt_input and prompt_input.strip():
|
| 230 |
processed_prompt = prompt_input.strip()
|
| 231 |
if not re.search(r'[.!?؟،:۔]$', processed_prompt): processed_prompt += "،"
|
| 232 |
final_text_for_api = f"{processed_prompt} {chunk_text.strip()}"
|
| 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 |
+
# --- START: تلاش برای تطبیق با ساختار قبلی generate_content ---
|
| 285 |
+
# این بخش بحرانی است و ممکن است با API جدید کار نکند اگر مدل و configها تغییر کرده باشند
|
| 286 |
+
# ** این GenerationConfig از کد قبلی شما میآید **
|
| 287 |
+
generation_config_for_tts = types.GenerationConfig( # در API جدید، این معمولاً GenerationConfig ساده است
|
| 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 |
+
# ** برای تست، فعلاً speech_config را حذف میکنیم و فقط متن و config پایه را ارسال میکنیم **
|
| 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, # یا [final_text_for_api]
|
| 339 |
+
generation_config=types.GenerationConfig( # استفاده از 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 |
+
# mime_type از پاسخ هم باید بررسی شود
|
| 358 |
+
# mime_type_from_response = inline_data.mime_type
|
| 359 |
+
# ext = mimetypes.guess_extension(mime_type_from_response) or ".wav"
|
| 360 |
+
ext = ".wav" # چون درخواست wav کردهایم
|
| 361 |
+
|
| 362 |
+
# if "audio/L" in mime_type_from_response and ext == ".wav": # این برای فرمت خاص قبلی بود
|
| 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):
|
| 369 |
try: os.remove(temp_fpath_for_chunk)
|
| 370 |
except OSError: pass
|
| 371 |
+
|
| 372 |
fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
|
| 373 |
if fpath:
|
| 374 |
generated_files.append(fpath); chunk_processed_successfully = True
|
| 375 |
_log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
|
| 376 |
if chunk_idx < len(text_chunks) - 1: time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
|
| 377 |
break
|
| 378 |
+
else:
|
| 379 |
+
_log(f" ⚠️ پاسخ API برای قطعه {chunk_idx+1} با کلید {key_display_num} بدون داده صوتی معتبر بود. پاسخ: {response.text if hasattr(response, 'text') else str(response)[:200]}")
|
| 380 |
+
|
| 381 |
except google_exceptions.ResourceExhausted as e_quota:
|
| 382 |
_log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
|
| 383 |
if attempt_num_for_chunk < max_attempts_for_chunk - 1:
|
| 384 |
_log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه."); time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
|
| 385 |
else: _log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند (خطای سهمیه)."); all_chunks_processed = False
|
| 386 |
+
|
| 387 |
except Exception as e_general:
|
| 388 |
+
error_type_name = type(e_general).__name__
|
| 389 |
+
_log(f" ❌ خطای عمومی ({error_type_name}) در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {str(e_general)[:200]}")
|
| 390 |
+
if "response_mime_type" in str(e_general).lower() or "modality" in str(e_general).lower():
|
| 391 |
+
_log(" این خطا ممکن است مربوط به عدم پشتیبانی ��دل از خروجی صوتی یا تنظیمات نادرست response_mime_type باشد.")
|
| 392 |
+
if "model" in str(e_general).lower() and "not found" in str(e_general).lower():
|
| 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
|
| 397 |
+
|
| 398 |
if chunk_processed_successfully: break
|
| 399 |
+
|
| 400 |
if not chunk_processed_successfully:
|
| 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 |
+
# ... (بقیه کد core_generate_audio برای ادغام و بازگرداندن فایل، بدون تغییر) ...
|
| 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
|
| 411 |
final_audio_file = None; final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
|
|
|
|
| 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
|
| 428 |
else:
|
| 429 |
_log("⚠️ pydub نیست. اولین قطعه ارائه میشود.")
|
|
|
|
| 435 |
for i_gf in range(1, len(generated_files)):
|
| 436 |
try: os.remove(generated_files[i_gf])
|
| 437 |
except: pass
|
| 438 |
+
except Exception as e_rename_single_no_pydub: _log(f"خطا در تغییر نام (بدون pydub): {e_rename_single_no_pydub}"); final_audio_file = generated_files[0]
|
| 439 |
elif len(generated_files) == 1:
|
| 440 |
try:
|
| 441 |
target_ext = os.path.splitext(generated_files[0])[1]; final_single_fn = f"{final_output_path_base}{target_ext}"
|
| 442 |
if os.path.exists(final_single_fn): os.remove(final_single_fn)
|
| 443 |
os.rename(generated_files[0], final_single_fn); final_audio_file = final_single_fn
|
| 444 |
+
except Exception as e_rename_single_final_file: _log(f"خطا در تغییر نام فایل تکی: {e_rename_single_final_file}"); final_audio_file = generated_files[0]
|
| 445 |
if final_audio_file and os.path.exists(final_audio_file): _log(f"✅ فایل نهایی: {os.path.basename(final_audio_file)}")
|
| 446 |
elif final_audio_file: _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!"); return None
|
| 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 |
else:
|
| 462 |
actual_text = text_to_speak
|
| 463 |
if not actual_text or not actual_text.strip(): _log("❌ متن ورودی خالی."); return None
|
| 464 |
+
if not GOOGLE_LIBS_AVAILABLE or not GENAI_MODEL_ACCESS_CONFIGURED : # بررس�� جدید
|
| 465 |
+
gr.Warning("خطای سیستمی: کتابخانههای مورد نیاز یا تنظیمات مدل به درستی بارگذاری نشدهاند.")
|
| 466 |
return None
|
| 467 |
if NUM_API_KEYS == 0:
|
| 468 |
gr.Warning("خطای سیستمی: کلید API موجود نیست.")
|
| 469 |
return None
|
| 470 |
+
final_path = core_generate_audio(actual_text, prompt_input, speaker_voice, temperature)
|
| 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 |
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 |
+
speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3") # speaker_voice دیگر به طور مستقیم به API ارسال نمیشود، اما در UI باقی میماند
|
| 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")
|
| 491 |
output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
|
| 492 |
generate_button.click(fn=gradio_tts_interface, inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio])
|
| 493 |
gr.Markdown("<h3 class='section-title-main-alpha' style='margin-top:2.5rem; text-align:center; border-bottom:none;'>نمونههای کاربردی</h3>", elem_id="examples_section_title_v3")
|
| 494 |
+
gr.Examples(examples=[[False,None,"سلام بر شما، امیدوارم روز خوبی داشته باشید.","با لحنی گرم و صمیمی.","Zephyr",0.85],[False,None,"این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.","با صدایی طبیعی و روان.","Charon",0.9],[False,None,"آیا میتوانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.","با کنجکاوی","Puck",0.95],[False,None,"این یک متن بسیار طولانی است.","با لحنی آرام و واضح","Achird",0.8],], inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio], fn=gradio_tts_interface, cache_examples=False )
|
| 495 |
gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
|
| 496 |
|
| 497 |
if __name__ == "__main__":
|
| 498 |
+
if GOOGLE_LIBS_AVAILABLE and GENAI_MODEL_ACCESS_CONFIGURED and NUM_API_KEYS > 0:
|
| 499 |
demo.launch()
|
| 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 را بررسی کنید.")
|