Hamed744 commited on
Commit
af0842c
·
verified ·
1 Parent(s): cf1843b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -64
app.py CHANGED
@@ -8,6 +8,8 @@ import time
8
  import zipfile
9
  from google import genai
10
  from google.genai import types
 
 
11
 
12
  try:
13
  from pydub import AudioSegment
@@ -15,37 +17,50 @@ try:
15
  except ImportError:
16
  PYDUB_AVAILABLE = False
17
 
18
- # --- START: منطق چرخش API Key ---
19
- GEMINI_API_KEYS = []
20
- i = 1
21
- while os.environ.get(f"GEMINI_API_KEY_{i}"):
22
- GEMINI_API_KEYS.append(os.environ.get(f"GEMINI_API_KEY_{i}"))
23
- i += 1
24
 
25
- NUM_API_KEYS = len(GEMINI_API_KEYS)
26
- CURRENT_KEY_INDEX = 0
 
 
27
 
28
- def _log(message):
29
- """تابع ساده شده برای لاگ کردن پیام‌ها به کنسول."""
30
- print(f"[لاگ آلفا TTS] {message}")
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- if NUM_API_KEYS == 0:
33
- _log("⛔️ خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!")
34
- _log(" لطفاً Secret ها را مانند GEMINI_API_KEY_1, GEMINI_API_KEY_2, ... در تنظیمات Space خود اضافه کنید.")
35
- else:
36
- _log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
37
 
38
  def get_next_api_key():
39
- global CURRENT_KEY_INDEX
40
- if NUM_API_KEYS == 0:
41
- _log("⚠️ تلاش برای گرفتن کلید API در حالی که هیچ کلیدی بارگذاری نشده است.")
42
- return None, None
43
-
44
- key_to_use = GEMINI_API_KEYS[CURRENT_KEY_INDEX % NUM_API_KEYS]
45
- key_display_index = (CURRENT_KEY_INDEX % NUM_API_KEYS) + 1
46
- CURRENT_KEY_INDEX += 1
47
- return key_to_use, key_display_index
48
- # --- END: منطق چرخش API Key ---
 
 
 
 
49
 
50
  SPEAKER_VOICES = [
51
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
@@ -64,7 +79,7 @@ def save_binary_file(file_name, data):
64
  with open(file_name, "wb") as f: f.write(data)
65
  return file_name
66
  except Exception as e:
67
- _log(f"❌ خطا در ذخیره فایل {file_name}: {e}")
68
  return None
69
 
70
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
@@ -106,31 +121,40 @@ def smart_text_split(text, max_size=3800):
106
  return final_chunks
107
 
108
  def merge_audio_files_func(file_paths, output_path):
109
- if not PYDUB_AVAILABLE: _log("⚠️ pydub برای ادغام در دسترس نیست."); return False
110
  try:
111
  combined = AudioSegment.empty()
112
  for i, fp in enumerate(file_paths):
113
  if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
114
- else: _log(f"⚠️ فایل برای ادغام پیدا نشد: {fp}")
115
  combined.export(output_path, format="wav")
116
  return True
117
- except Exception as e: _log(f"❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
118
 
119
- # --- START: منطق جدید تولید صدا با قابلیت تلاش مجدد ---
120
 
121
  def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp):
122
  """
123
  یک قطعه صوتی را با قابلیت تلاش مجدد با کلیدهای مختلف API تولید می‌کند.
124
- اگر یک کلید ناموفق بود، به طور خودکار کلید بعدی را امتحان می‌کند.
125
  """
126
- # تلاش برای تولید صدا به تعداد کلیدهای موجود
127
- for i in range(NUM_API_KEYS):
 
 
 
 
 
 
 
128
  selected_api_key, key_idx_display = get_next_api_key()
 
 
129
  if not selected_api_key:
130
- _log("⚠️ هیچ کلید API برای تلاش باقی نمانده است.")
131
- continue
132
 
133
- _log(f"⚙️ تلاش برای تولید قطعه با کلید API شماره {key_idx_display} (...{selected_api_key[-4:]})")
134
 
135
  try:
136
  client = genai.Client(api_key=selected_api_key)
@@ -143,35 +167,35 @@ def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp):
143
  response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
144
 
145
  if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
146
- _log(f"✅ قطعه با موفقیت توسط کلید شماره {key_idx_display} تولید شد.")
147
  return response.candidates[0].content.parts[0].inline_data
148
  else:
149
- _log(f"⚠️ پاسخ API برای قطعه با کلید شماره {key_idx_display} بدون داده صوتی بود. تلاش با کلید بعدی...")
150
 
151
  except Exception as e:
152
- _log(f"❌ خطا در تولید قطعه با کلید شماره {key_idx_display}: {e}. تلاش با کلید بعدی...")
153
 
154
- _log("❌ تمام کلیدهای API امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
155
  return None
156
 
157
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
158
- _log("🚀 شروع فرآیند تولید صدا با قابلیت تعویض کلید خودکار...")
159
 
160
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
161
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
162
 
163
  if not text_input or not text_input.strip():
164
- _log("❌ متن ورودی خالی است.")
165
  return None
166
 
167
  text_chunks = smart_text_split(text_input, max_chunk)
168
  if not text_chunks:
169
- _log("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
170
  return None
171
 
172
  generated_files = []
173
  for i, chunk in enumerate(text_chunks):
174
- _log(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...")
175
 
176
  inline_data = generate_audio_chunk_with_retry(chunk, prompt_input, selected_voice, temperature_val)
177
 
@@ -187,17 +211,17 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
187
  if fpath:
188
  generated_files.append(fpath)
189
  else:
190
- _log(f"❌ موفق به ذخیره فایل برای قطعه {i+1} نشدیم. این قطعه نادیده گرفته می‌شود.")
191
  continue
192
  else:
193
- _log(f"🛑 فرآیند متوقف شد زیرا تولید قطعه {i+1} با تمام کلیدهای موجود ناموفق بود.")
194
- break
195
 
196
  if i < len(text_chunks) - 1 and len(text_chunks) > 1:
197
  time.sleep(sleep_time)
198
 
199
  if not generated_files:
200
- _log(f"❌ هیچ فایل صوتی تولید نشد.")
201
  return None
202
 
203
  final_audio_file = None
@@ -218,7 +242,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
218
  os.rename(generated_files[0], renamed_first_chunk)
219
  final_audio_file = renamed_first_chunk
220
  except Exception as e_rename:
221
- _log(f"خطا در تغییر نام فایل اولین قطعه (پس از ادغام ناموفق): {e_rename}")
222
  final_audio_file = generated_files[0]
223
 
224
  for fp_cleanup in generated_files:
@@ -228,7 +252,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
228
  except: pass
229
 
230
  else:
231
- _log("⚠️ pydub در دسترس نیست. اولین قطعه صوتی ارائه می‌شود.")
232
  if generated_files:
233
  try:
234
  target_ext = os.path.splitext(generated_files[0])[1]
@@ -240,7 +264,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
240
  try: os.remove(generated_files[i_gf])
241
  except: pass
242
  except Exception as e_rename_single:
243
- _log(f"خطا در تغییر نام فایل اولین قطعه (بدون pydub): {e_rename_single}")
244
  final_audio_file = generated_files[0]
245
 
246
  elif len(generated_files) == 1:
@@ -251,16 +275,16 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
251
  os.rename(generated_files[0], final_single_fn)
252
  final_audio_file = final_single_fn
253
  except Exception as e_rename_single_final:
254
- _log(f"خطا در تغییر نام فایل تکی نهایی: {e_rename_single_final}")
255
  final_audio_file = generated_files[0]
256
 
257
  if final_audio_file and os.path.exists(final_audio_file):
258
- _log(f"✅ فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_audio_file)}")
259
  elif final_audio_file:
260
- _log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!")
261
  return None
262
  else:
263
- _log(f"❓ وضعیت نامشخص برای فایل نهایی.")
264
  return None
265
 
266
  return final_audio_file
@@ -273,17 +297,27 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
273
  if uploaded_file:
274
  try:
275
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
276
- if not actual_text: _log("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
277
- except Exception as e: _log(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
278
- else: _log("❌ گزینه استفاده از فایل انتخاب شده اما فایلی آپلود نشده."); return None
279
  else:
280
  actual_text = text_to_speak
281
- if not actual_text or not actual_text.strip(): _log("❌ متن ورودی برای تبدیل خالی است."); return None
282
 
283
  final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
284
  return final_path
285
 
286
- # --- CSS ---
 
 
 
 
 
 
 
 
 
 
287
  custom_css_inspired_by_image = f"""
288
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
289
  :root {{
@@ -401,7 +435,10 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
401
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
402
 
403
  if __name__ == "__main__":
404
- if NUM_API_KEYS > 0 :
405
- demo.launch()
 
 
 
406
  else:
407
- _log("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")
 
8
  import zipfile
9
  from google import genai
10
  from google.genai import types
11
+ import threading # اضافه شد: برای KEY_LOCK و ریسه
12
+ import logging # اضافه شد: برای لاگ استاندارد
13
 
14
  try:
15
  from pydub import AudioSegment
 
17
  except ImportError:
18
  PYDUB_AVAILABLE = False
19
 
20
+ # --- START: پیکربندی لاگینگ ---
21
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
22
+ # --- END: پیکربندی لاگینگ ---
 
 
 
23
 
24
+ # --- START: منطق جدید مدیریت API Key (مشابه کد قبلی شما) ---
25
+ ALL_API_KEYS: list[str] = []
26
+ NEXT_KEY_INDEX: int = 0
27
+ KEY_LOCK: threading.Lock = threading.Lock() # برای اطمینان از ایمنی تردها هنگام به روز رسانی ایندکس
28
 
29
+ def _init_api_keys():
30
+ """
31
+ کلیدهای API را از یک متغیر محیطی واحد شناسایی و مرتب می‌کند.
32
+ این تابع باید یک بار هنگام شروع برنامه اجرا شود.
33
+ """
34
+ global ALL_API_KEYS
35
+
36
+ all_keys_string = os.environ.get("ALL_GEMINI_API_KEYS")
37
+
38
+ if all_keys_string:
39
+ ALL_API_KEYS = [key.strip() for key in all_keys_string.split(',') if key.strip()]
40
+
41
+ logging.info(f"✅ تعداد {len(ALL_API_KEYS)} کلید API جیمینای بارگذاری شد.")
42
+ if not ALL_API_KEYS:
43
+ logging.warning("⛔️ خطای حیاتی: هیچ Secret با نام ALL_GEMINI_API_KEYS یافت نشد!")
44
+ logging.warning(" لطفاً Secret را به عنوان یک رشته با کاما جدا شده (مثال: key1,key2,key3) در تنظیمات Space خود اضافه کنید.")
45
 
46
+ # فراخوانی تابع شناسایی کلیدها در ابتدای برنامه
47
+ _init_api_keys()
 
 
 
48
 
49
  def get_next_api_key():
50
+ """
51
+ کلید API بعدی را به صورت چرخشی برمی‌گرداند.
52
+ """
53
+ global NEXT_KEY_INDEX, ALL_API_KEYS, KEY_LOCK
54
+
55
+ with KEY_LOCK: # اطمینان از اینکه تنها یک ترد در هر زمان به ایندکس دسترسی دارد
56
+ if not ALL_API_KEYS:
57
+ return None, None
58
+
59
+ key_to_use = ALL_API_KEYS[NEXT_KEY_INDEX % len(ALL_API_KEYS)]
60
+ key_display_index = (NEXT_KEY_INDEX % len(ALL_API_KEYS)) + 1
61
+ NEXT_KEY_INDEX += 1
62
+ return key_to_use, key_display_index
63
+ # --- END: منطق جدید مدیریت API Key ---
64
 
65
  SPEAKER_VOICES = [
66
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
 
79
  with open(file_name, "wb") as f: f.write(data)
80
  return file_name
81
  except Exception as e:
82
+ logging.error(f"❌ خطا در ذخیره فایل {file_name}: {e}")
83
  return None
84
 
85
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
 
121
  return final_chunks
122
 
123
  def merge_audio_files_func(file_paths, output_path):
124
+ if not PYDUB_AVAILABLE: logging.warning("⚠️ pydub برای ادغام در دسترس نیست."); return False
125
  try:
126
  combined = AudioSegment.empty()
127
  for i, fp in enumerate(file_paths):
128
  if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
129
+ else: logging.warning(f"⚠️ فایل برای ادغام پیدا نشد: {fp}")
130
  combined.export(output_path, format="wav")
131
  return True
132
+ except Exception as e: logging.error(f"❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
133
 
134
+ # --- START: منطق تولید صدا با قابلیت تلاش مجدد با کلیدهای چرخشی ---
135
 
136
  def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp):
137
  """
138
  یک قطعه صوتی را با قابلیت تلاش مجدد با کلیدهای مختلف API تولید می‌کند.
139
+ اگر یک کلید ناموفق بود، به طور خودکار کلید بعدی را امتحان می‌کند تا تمام کلیدها بررسی شوند.
140
  """
141
+ # تلاش برای تولید صدا به تعداد کلیدهای موجود در ALL_API_KEYS
142
+ # از len(ALL_API_KEYS) به جای NUM_API_KEYS استفاده میکنیم چون NUM_API_KEYS حذف شده است
143
+ if not ALL_API_KEYS:
144
+ logging.error("❌ هیچ کلید API برای تولید صدا در دسترس نیست.")
145
+ return None
146
+
147
+ # این حلقه به تعداد کل کلیدهای موجود در ALL_API_KEYS تلاش می‌کند.
148
+ # در هر دور، یک کلید جدید و چرخشی از get_next_api_key دریافت می‌شود.
149
+ for _ in range(len(ALL_API_KEYS)):
150
  selected_api_key, key_idx_display = get_next_api_key()
151
+
152
+ # اگر به دلیلی get_next_api_key، کلیدی برنگرداند (نبود کلید یا خطای داخلی)
153
  if not selected_api_key:
154
+ logging.warning("⚠️ get_next_api_key هیچ کلیدی برنگرداند. تلاش‌های باقیمانده نادیده گرفته می‌شوند.")
155
+ break
156
 
157
+ logging.info(f"⚙️ تلاش برای تولید قطعه با کلید API شماره {key_idx_display} (...{selected_api_key[-4:]})")
158
 
159
  try:
160
  client = genai.Client(api_key=selected_api_key)
 
167
  response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
168
 
169
  if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
170
+ logging.info(f"✅ قطعه با موفقیت توسط کلید شماره {key_idx_display} تولید شد.")
171
  return response.candidates[0].content.parts[0].inline_data
172
  else:
173
+ logging.warning(f"⚠️ پاسخ API برای قطعه با کلید شماره {key_idx_display} بدون داده صوتی بود. تلاش با کلید بعدی...")
174
 
175
  except Exception as e:
176
+ logging.error(f"❌ خطا در تولید قطعه با کلید شماره {key_idx_display}: {e}. تلاش با کلید بعدی...")
177
 
178
+ logging.error("❌ تمام کلیدهای API امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
179
  return None
180
 
181
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
182
+ logging.info("🚀 شروع فرآیند تولید صدا با قابلیت تعویض کلید خودکار...")
183
 
184
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
185
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
186
 
187
  if not text_input or not text_input.strip():
188
+ logging.error("❌ متن ورودی خالی است.")
189
  return None
190
 
191
  text_chunks = smart_text_split(text_input, max_chunk)
192
  if not text_chunks:
193
+ logging.error("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
194
  return None
195
 
196
  generated_files = []
197
  for i, chunk in enumerate(text_chunks):
198
+ logging.info(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...")
199
 
200
  inline_data = generate_audio_chunk_with_retry(chunk, prompt_input, selected_voice, temperature_val)
201
 
 
211
  if fpath:
212
  generated_files.append(fpath)
213
  else:
214
+ logging.error(f"❌ موفق به ذخیره فایل برای قطعه {i+1} نشدیم. این قطعه نادیده گرفته می‌شود.")
215
  continue
216
  else:
217
+ logging.error(f"🛑 فرآیند متوقف شد زیرا تولید قطعه {i+1} با تمام کلیدهای موجود ناموفق بود.")
218
+ break # اگر یک قطعه با هیچ کلیدی تولید نشد، کل فرآیند را متوقف می کنیم.
219
 
220
  if i < len(text_chunks) - 1 and len(text_chunks) > 1:
221
  time.sleep(sleep_time)
222
 
223
  if not generated_files:
224
+ logging.error(f"❌ هیچ فایل صوتی تولید نشد.")
225
  return None
226
 
227
  final_audio_file = None
 
242
  os.rename(generated_files[0], renamed_first_chunk)
243
  final_audio_file = renamed_first_chunk
244
  except Exception as e_rename:
245
+ logging.error(f"خطا در تغییر نام فایل اولین قطعه (پس از ادغام ناموفق): {e_rename}")
246
  final_audio_file = generated_files[0]
247
 
248
  for fp_cleanup in generated_files:
 
252
  except: pass
253
 
254
  else:
255
+ logging.warning("⚠️ pydub در دسترس نیست. اولین قطعه صوتی ارائه می‌شود.")
256
  if generated_files:
257
  try:
258
  target_ext = os.path.splitext(generated_files[0])[1]
 
264
  try: os.remove(generated_files[i_gf])
265
  except: pass
266
  except Exception as e_rename_single:
267
+ logging.error(f"خطا در تغییر نام فایل اولین قطعه (بدون pydub): {e_rename_single}")
268
  final_audio_file = generated_files[0]
269
 
270
  elif len(generated_files) == 1:
 
275
  os.rename(generated_files[0], final_single_fn)
276
  final_audio_file = final_single_fn
277
  except Exception as e_rename_single_final:
278
+ logging.error(f"خطا در تغییر نام فایل تکی نهایی: {e_rename_single_final}")
279
  final_audio_file = generated_files[0]
280
 
281
  if final_audio_file and os.path.exists(final_audio_file):
282
+ logging.info(f"✅ فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_audio_file)}")
283
  elif final_audio_file:
284
+ logging.warning(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!")
285
  return None
286
  else:
287
+ logging.error(f"❓ وضعیت نامشخص برای فایل نهایی.")
288
  return None
289
 
290
  return final_audio_file
 
297
  if uploaded_file:
298
  try:
299
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
300
+ if not actual_text: logging.error("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
301
+ except Exception as e: logging.error(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
302
+ else: logging.warning("❌ گزینه استفاده از فایل انتخاب شده اما فایلی آپلود نشده."); return None
303
  else:
304
  actual_text = text_to_speak
305
+ if not actual_text or not actual_text.strip(): logging.warning("❌ متن ورودی برای تبدیل خالی است."); return None
306
 
307
  final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
308
  return final_path
309
 
310
+ # --- تابع جدید برای ریست خودکار هر 24 ساعت ---
311
+ def auto_restart_service():
312
+ RESTART_INTERVAL_SECONDS = 24 * 60 * 60
313
+ logging.info(f"سرویس برای ری‌استارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمان‌بندی شده است.")
314
+ time.sleep(RESTART_INTERVAL_SECONDS)
315
+ logging.info(f"زمان ری‌استارت خودکار فرا رسیده است. برنامه برای ری‌استارت خارج می‌شود...")
316
+ os._exit(1)
317
+ # --- END: تابع ری‌استارت خودکار ---
318
+
319
+
320
+ # --- CSS و Gradio UI (بدون تغییر) ---
321
  custom_css_inspired_by_image = f"""
322
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
323
  :root {{
 
435
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
436
 
437
  if __name__ == "__main__":
438
+ threading.Thread(target=auto_restart_service, daemon=True, name="AutoRestartThread").start()
439
+
440
+ # استفاده از len(ALL_API_KEYS) به جای NUM_API_KEYS قدیمی
441
+ if len(ALL_API_KEYS) > 0 :
442
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
443
  else:
444
+ logging.critical("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")