Ezmary commited on
Commit
ab013ee
·
verified ·
1 Parent(s): 20cf1e7

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +71 -143
templates/index.html CHANGED
@@ -88,90 +88,18 @@
88
  textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; }
89
  textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); }
90
 
91
- .slider-group {
92
- margin-top: 2rem;
93
- margin-bottom: 1.5rem;
94
- }
95
- .slider-label-container {
96
- display: flex;
97
- justify-content: space-between;
98
- align-items: center;
99
- margin-bottom: 1rem;
100
- }
101
- .slider-label {
102
- font-weight: 600;
103
- color: var(--text-secondary);
104
- font-size: 0.95rem;
105
- }
106
- .slider-value {
107
- font-weight: 700;
108
- font-size: 1rem;
109
- color: var(--accent-primary);
110
- background-color: var(--input-bg);
111
- padding: 0.25rem 0.75rem;
112
- border-radius: 8px;
113
- border: 1px solid var(--input-border);
114
- min-width: 80px;
115
- text-align: center;
116
- }
117
- .slider-wrapper {
118
- display: flex;
119
- align-items: center;
120
- gap: 1rem;
121
- }
122
- .slider-icon {
123
- width: 22px;
124
- height: 22px;
125
- color: var(--text-tertiary);
126
- flex-shrink: 0;
127
- opacity: 0.8;
128
- }
129
- input[type="range"] {
130
- -webkit-appearance: none;
131
- appearance: none;
132
- flex-grow: 1;
133
- width: 100%;
134
- height: 12px;
135
- background: var(--input-bg);
136
- border-radius: var(--radius-input);
137
- outline: none;
138
- cursor: pointer;
139
- direction: rtl;
140
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
141
- transition: var(--transition-smooth);
142
- }
143
- input[type="range"]::-webkit-slider-thumb {
144
- -webkit-appearance: none;
145
- appearance: none;
146
- width: 24px;
147
- height: 24px;
148
- background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary));
149
- border-radius: 50%;
150
- border: 3px solid white;
151
- box-shadow: var(--shadow-md);
152
- transition: all 0.2s ease-in-out;
153
- margin-top: -6px;
154
- }
155
- input[type="range"]::-moz-range-thumb {
156
- width: 24px;
157
- height: 24px;
158
- background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary));
159
- border-radius: 50%;
160
- border: 3px solid white;
161
- box-shadow: var(--shadow-md);
162
- transition: all 0.2s ease-in-out;
163
- }
164
- input[type="range"]:hover {
165
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow);
166
- }
167
- input[type="range"]:hover::-webkit-slider-thumb {
168
- box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md);
169
- transform: scale(1.1);
170
- }
171
- input[type="range"]:active::-webkit-slider-thumb {
172
- transform: scale(1.2);
173
- box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md);
174
- }
175
 
176
  #generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px rgba(15, 212, 168, 0.25); margin-top: 1rem; }
177
  #generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
@@ -190,7 +118,8 @@
190
  .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
191
  @keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
192
  .noise-layer, .sketch-layer, .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
193
- .noise-layer { background: url('data:image/svg+xml;utf8,<svg xmlns="http="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; }
 
194
  @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
195
  .sketch-layer { filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; }
196
  @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
@@ -220,12 +149,8 @@
220
  .gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
221
  .gpu-error-message-container .error-content li { margin-bottom: 6px; }
222
  .gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
223
- .gpu-error-actions .action-button { padding: 12px 20px; border: none; border-radius: var(--igadlm-alpha-border-radius-md); font-size: 0.95em; font-weight: 600; cursor: pointer; font-family: inherit; width: 100%; box-shadow: var(--shadow-md); transition: var(--transition-smooth); text-transform: none; letter-spacing: normal; }
224
- .gpu-error-actions .action-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
225
  .gpu-error-actions .action-button.back-button { background-color: var(--panel-bg); color: var(--text-tertiary); border: 1px solid var(--panel-border); flex-grow: 0.45; }
226
- .gpu-error-actions .action-button.back-button:hover:not(:disabled) { background-color: var(--input-bg); }
227
  .gpu-error-actions .action-button.retry-button { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary) 0%, var(--igadlm-alpha-color-secondary) 100%); color: white; flex-grow: 1; }
228
- .gpu-error-actions .action-button.retry-button:hover:not(:disabled) { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary-darker) 0%, var(--igadlm-alpha-color-secondary-darker) 100%); }
229
 
230
  #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
231
  #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
@@ -493,18 +418,22 @@
493
  const durationSlider = document.getElementById('durationSlider');
494
  const durationValueDisplay = document.getElementById('durationValue');
495
 
 
496
  const VIDEO_SPACE_NAME = "mrbui1990/wan2-2-fp8da-aoti-faster";
497
- const TRANSLATOR_SPACE_URL_BASE = "https://hamed744-translate-tts-aloha.hf.space/gradio_api";
 
 
498
  const FN_INDEX_TRANSLATE_SPEAK = 1;
499
  const DEFAULT_TTS_VOICE_TRANSLATOR = "انگلیسی (آمریکا) - جنی (زن)";
 
500
  const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走";
501
  const DEFAULT_PROMPT_TEXT = "";
502
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
503
  const DEFAULT_VIDEO_DURATION = 4.3;
504
- // --- START: TIMEOUT LOGIC ---
505
- // زمان وقفه به میلی‌ثانیه: 2.5 دقیقه
506
- const GENERATION_TIMEOUT = 150000;
507
- // --- END: TIMEOUT LOGIC ---
508
 
509
  let lastAttemptedVideoPayload = null;
510
 
@@ -596,34 +525,34 @@
596
  updateSliderStyle(durationSlider);
597
  updateUIForSubscriptionStatus(userSubscriptionStatus);
598
  }
599
-
600
- async function translatePersianToEnglish(persianText) {
601
- let currentTranslatorSessionHash = generateRandomHash();
602
- const payload = { data: [persianText, DEFAULT_TTS_VOICE_TRANSLATOR, 0, 0, 0], fn_index: FN_INDEX_TRANSLATE_SPEAK, session_hash: currentTranslatorSessionHash };
 
603
  if (aiLoader.style.display !== 'none') setAiLoaderText("در حال ترجمه دستور شما...");
604
- try {
605
- await fetch(`${TRANSLATOR_SPACE_URL_BASE}/queue/join?`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
606
- return new Promise((resolve, reject) => {
607
- const eventSource = new EventSource(`${TRANSLATOR_SPACE_URL_BASE}/queue/data?session_hash=${currentTranslatorSessionHash}`);
608
- eventSource.onmessage = function(event) {
609
- const data = JSON.parse(event.data);
610
- if (data.msg === "process_completed") {
611
- eventSource.close();
612
- if (data.success && data.output?.data?.[0]) {
613
- resolve(data.output.data[0]);
614
- } else {
615
- reject(new Error(data.output?.error || "ترجمه ناموفق بود."));
616
- }
617
- }
618
- };
619
- eventSource.onerror = () => { eventSource.close(); reject(new Error("خطا در ارتباط با سرور مترجم.")); };
620
- });
621
- } catch (error) { throw error; }
622
  }
623
 
624
  async function handleSuccessfulVideoGeneration(result) {
625
  aiLoader.style.display = 'none';
626
- if (!result.data?.[0]?.video?.url) {
627
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
628
  return;
629
  }
@@ -722,44 +651,43 @@
722
 
723
  lastAttemptedVideoPayload = { persianPrompt, imageSourceFile: imageFile, duration };
724
 
 
 
725
  try {
726
- const englishPrompt = await translatePersianToEnglish(persianPrompt);
727
  addStatusMessage(`ترجمه: ${englishPrompt}`, 'info');
 
 
 
 
 
 
728
  setAiLoaderText("در حال ساخت ویدیو...");
729
-
730
  const client = await Client.connect(VIDEO_SPACE_NAME);
731
 
732
- // --- START: TIMEOUT LOGIC ---
733
- // ایجاد یک پرامیس که پس از زمان مشخص شده، با خطا رد می‌شود
734
- const timeoutPromise = new Promise((_, reject) => {
735
- setTimeout(() => {
736
- reject(new Error('پردازش ویدیو بیش از 2.5 دقیقه طول کشید. لطفاً مجدداً تلاش کنید. ممکن است سرور شلوغ باشد.'));
737
- }, GENERATION_TIMEOUT);
738
- });
739
 
740
- // ایجاد پرامیس اصلی برای ساخت ویدیو
741
  const videoGenerationPromise = client.predict(0, [
742
- imageFile, // 0: input_image
743
- englishPrompt, // 1: prompt
744
- 6, // 2: steps
745
- DEFAULT_NEGATIVE_PROMPT,// 3: negative_prompt
746
- duration, // 4: duration_seconds
747
- 1, // 5: guidance_scale
748
- 1, // 6: guidance_scale_2
749
- 0, // 7: seed (0 for random)
750
- true // 8: randomize_seed
751
  ]);
752
-
753
- // رقابت بین پرامیس ویدیو و پرامیس تایم‌اوت
754
- // هر کدام زودتر تمام شود، نتیجه آن برگردانده می‌شود
755
- const result = await Promise.race([videoGenerationPromise, timeoutPromise]);
756
- // --- END: TIMEOUT LOGIC ---
757
 
 
758
  await handleSuccessfulVideoGeneration(result);
759
 
760
  } catch (errorCaught) {
761
- const isTranslationErr = errorCaught.message.toLowerCase().includes("مترجم");
762
- showCriticalError(`خطا: ${errorCaught.message}`, isTranslationErr);
 
763
  }
764
  };
765
 
 
88
  textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; }
89
  textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); }
90
 
91
+ .slider-group { margin-top: 2rem; margin-bottom: 1.5rem; }
92
+ .slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
93
+ .slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; }
94
+ .slider-value { font-weight: 700; font-size: 1rem; color: var(--accent-primary); background-color: var(--input-bg); padding: 0.25rem 0.75rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 80px; text-align: center; }
95
+ .slider-wrapper { display: flex; align-items: center; gap: 1rem; }
96
+ .slider-icon { width: 22px; height: 22px; color: var(--text-tertiary); flex-shrink: 0; opacity: 0.8; }
97
+ input[type="range"] { -webkit-appearance: none; appearance: none; flex-grow: 1; width: 100%; height: 12px; background: var(--input-bg); border-radius: var(--radius-input); outline: none; cursor: pointer; direction: rtl; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); transition: var(--transition-smooth); }
98
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; margin-top: -6px; }
99
+ input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; }
100
+ input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); }
101
+ input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); }
102
+ input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  #generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px rgba(15, 212, 168, 0.25); margin-top: 1rem; }
105
  #generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
 
118
  .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
119
  @keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
120
  .noise-layer, .sketch-layer, .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
121
+ /* FIX xmlns در SVG اینجا اعمال شده */
122
+ .noise-layer { background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; }
123
  @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
124
  .sketch-layer { filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; }
125
  @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
 
149
  .gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
150
  .gpu-error-message-container .error-content li { margin-bottom: 6px; }
151
  .gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
 
 
152
  .gpu-error-actions .action-button.back-button { background-color: var(--panel-bg); color: var(--text-tertiary); border: 1px solid var(--panel-border); flex-grow: 0.45; }
 
153
  .gpu-error-actions .action-button.retry-button { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary) 0%, var(--igadlm-alpha-color-secondary) 100%); color: white; flex-grow: 1; }
 
154
 
155
  #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
156
  #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
 
418
  const durationSlider = document.getElementById('durationSlider');
419
  const durationValueDisplay = document.getElementById('durationValue');
420
 
421
+ // ====== تنظیمات مدل‌ها ======
422
  const VIDEO_SPACE_NAME = "mrbui1990/wan2-2-fp8da-aoti-faster";
423
+
424
+ // مترجم: به‌جای SSE از Gradio Client استفاده می‌کنیم (پایدار روی موبایل)
425
+ const TRANSLATOR_SPACE_NAME = "hamed744/translate-tts-aloha";
426
  const FN_INDEX_TRANSLATE_SPEAK = 1;
427
  const DEFAULT_TTS_VOICE_TRANSLATOR = "انگلیسی (آمریکا) - جنی (زن)";
428
+
429
  const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走";
430
  const DEFAULT_PROMPT_TEXT = "";
431
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
432
  const DEFAULT_VIDEO_DURATION = 4.3;
433
+
434
+ // --- TIMEOUTS ---
435
+ const GENERATION_TIMEOUT = 150000; // 2.5 دقیقه برای ساخت ویدیو
436
+ const TRANSLATION_TIMEOUT = 20000; // 20 ثانیه برای ترجمه
437
 
438
  let lastAttemptedVideoPayload = null;
439
 
 
525
  updateSliderStyle(durationSlider);
526
  updateUIForSubscriptionStatus(userSubscriptionStatus);
527
  }
528
+
529
+ // ==== نسخه جدید مترجم با Gradio Client + تایم‌اوت ====
530
+ async function translatePersianToEnglish(persianText, { timeoutMs = TRANSLATION_TIMEOUT } = {}) {
531
+ if (!persianText?.trim()) throw new Error("متن ترجمه خالی است.");
532
+
533
  if (aiLoader.style.display !== 'none') setAiLoaderText("در حال ترجمه دستور شما...");
534
+ const translator = await Client.connect(TRANSLATOR_SPACE_NAME);
535
+
536
+ const predictPromise = translator.predict(FN_INDEX_TRANSLATE_SPEAK, [
537
+ persianText,
538
+ DEFAULT_TTS_VOICE_TRANSLATOR,
539
+ 0, 0, 0
540
+ ]);
541
+
542
+ const timeoutPromise = new Promise((_, reject) =>
543
+ setTimeout(() => reject(new Error("مهلت ترجمه به پایان رسید.")), timeoutMs)
544
+ );
545
+
546
+ const resp = await Promise.race([predictPromise, timeoutPromise]);
547
+
548
+ const english = resp?.data?.[0];
549
+ if (!english) throw new Error("ترجمه نامعتبر بود.");
550
+ return english;
 
551
  }
552
 
553
  async function handleSuccessfulVideoGeneration(result) {
554
  aiLoader.style.display = 'none';
555
+ if (!result?.data?.[0]?.video?.url) {
556
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
557
  return;
558
  }
 
651
 
652
  lastAttemptedVideoPayload = { persianPrompt, imageSourceFile: imageFile, duration };
653
 
654
+ // ---- مرحله ترجمه با فالبک ----
655
+ let promptForVideo = persianPrompt;
656
  try {
657
+ const englishPrompt = await translatePersianToEnglish(persianPrompt, { timeoutMs: TRANSLATION_TIMEOUT });
658
  addStatusMessage(`ترجمه: ${englishPrompt}`, 'info');
659
+ promptForVideo = englishPrompt;
660
+ } catch (e) {
661
+ addStatusMessage('ترجمه به انگلیسی ناموفق/کند بود؛ با همان متن فارسی ادامه می‌دهیم.', 'error');
662
+ }
663
+
664
+ try {
665
  setAiLoaderText("در حال ساخت ویدیو...");
 
666
  const client = await Client.connect(VIDEO_SPACE_NAME);
667
 
668
+ const timeoutPromise = new Promise((_, reject) =>
669
+ setTimeout(() => reject(new Error('پردازش ویدیو بیش از 2.5 دقیقه طول کشید.')), GENERATION_TIMEOUT)
670
+ );
 
 
 
 
671
 
 
672
  const videoGenerationPromise = client.predict(0, [
673
+ imageFile, // 0: input_image
674
+ promptForVideo, // 1: prompt
675
+ 6, // 2: steps
676
+ DEFAULT_NEGATIVE_PROMPT, // 3: negative_prompt
677
+ duration, // 4: duration_seconds
678
+ 1, // 5: guidance_scale
679
+ 1, // 6: guidance_scale_2
680
+ 0, // 7: seed (0 = random)
681
+ true // 8: randomize_seed
682
  ]);
 
 
 
 
 
683
 
684
+ const result = await Promise.race([videoGenerationPromise, timeoutPromise]);
685
  await handleSuccessfulVideoGeneration(result);
686
 
687
  } catch (errorCaught) {
688
+ showCriticalError(`خطا: ${errorCaught.message}`);
689
+ } finally {
690
+ generateButton.disabled = false;
691
  }
692
  };
693