Hamed744 commited on
Commit
abc7c31
·
verified ·
1 Parent(s): e60f4e8

Update Yek.html

Browse files
Files changed (1) hide show
  1. Yek.html +354 -638
Yek.html CHANGED
@@ -1,660 +1,376 @@
1
- <p>&nbsp;</p>
2
- <style>
 
 
 
 
 
 
 
3
  :root {
4
- --color-primary: #667EEA; /* بنفش آبی ملایم */
5
- --color-secondary: #764BA2; /* بنفش تیره‌تر */
6
- --color-text: #2D3748; /* متن اصلی تیره */
7
- --color-text-muted: #718096; /* متن کم‌رنگ‌تر */
8
- -- rozmowy: #F7FAFC; /* پس‌زمینه روشن برای بخش‌ها */
9
- --color-border: #E2E8F0;
10
- --color-white: #FFFFFF;
11
- --color-success: #48BB78;
12
- --color-error: #F56565;
13
- --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
14
- --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
15
- --border-radius-md: 10px;
16
- --border-radius-lg: 16px;
 
 
 
17
  }
18
 
19
- * { box-sizing: border-box; margin: 0; padding: 0; }
20
-
21
  body {
22
- background-color: var(--color-white); /* پس‌زمینه ساده سفید */
23
- font-family: 'Vazirmatn', sans-serif;
24
- color: var(--color-text);
25
- line-height: 1.6;
26
- }
27
-
28
- .ltx-video-creator-app {
29
- width: 100%;
30
- max-width: 500px;
31
- margin: 20px auto;
32
- padding: 0 10px;
33
- }
34
-
35
- .app-header {
36
- text-align: center;
37
- margin-bottom: 25px;
38
- }
39
- .app-header h1 {
40
- font-size: 1.9em;
41
- font-weight: 700;
42
- background: linear-gradient(45deg, var(--color-primary), var(--color-secondary));
43
- -webkit-background-clip: text;
44
- -webkit-text-fill-color: transparent;
45
- display: flex;
46
- align-items: center;
47
- justify-content: center;
48
- margin-bottom: 8px;
49
- }
50
- .app-header h1 img {
51
- width: 32px;
52
- height: 32px;
53
- margin-left: 10px;
54
- }
55
- .app-header p {
56
- font-size: 0.95em;
57
- color: var(--color-text-muted);
58
- }
59
-
60
- .form-section-card {
61
- background-color: var(--color-white);
62
- padding: 25px;
63
- border-radius: var(--border-radius-lg);
64
- box-shadow: var(--shadow-lg);
65
- border: 1px solid var(--color-border);
66
- }
67
-
68
- .mode-selector {
69
- display: flex;
70
- background-color: var(--color-bg-light);
71
- border-radius: var(--border-radius-md);
72
- padding: 4px;
73
- margin-bottom: 20px;
74
- border: 1px solid var(--color-border);
75
- }
76
- .mode-button {
77
- flex: 1;
78
- padding: 8px 10px;
79
- background-color: transparent;
80
- border: none;
81
- cursor: pointer;
82
- font-family: inherit;
83
- font-size: 0.85rem;
84
- font-weight: 600;
85
- color: var(--color-text-muted);
86
- border-radius: 8px;
87
- transition: all 0.25s ease;
88
- }
89
- .mode-button.active {
90
- background-color: var(--color-primary);
91
- color: var(--color-white);
92
- box-shadow: 0 3px 8px rgba(102, 126, 234, 0.3);
93
- }
94
-
95
- .form-group {
96
- margin-bottom: 18px;
97
- }
98
- label {
99
- display: block;
100
- font-size: 0.8rem;
101
- text-transform: uppercase;
102
- letter-spacing: 0.5px;
103
- color: var(--color-text-muted);
104
- margin-bottom: 6px;
105
- font-weight: 600;
106
- }
107
- input[type="text"], textarea, input[type="number"] {
108
- width: 100%;
109
- padding: 11px 14px;
110
- border: 1px solid var(--color-border);
111
- border-radius: var(--border-radius-md);
112
- font-size: 0.9rem;
113
- background-color: var(--color-bg-light);
114
- color: var(--color-text);
115
- font-family: inherit;
116
- }
117
- input[type="text"]::placeholder, textarea::placeholder {
118
- color: #A0AEC0;
119
- }
120
-
121
- input[type="file"] {
122
- width: 100%;
123
- padding: 10px;
124
- border: 1px solid var(--color-border);
125
- border-radius: var(--border-radius-md);
126
- font-size: 0.9em;
127
- background-color: var(--color-bg-light);
128
- font-family: inherit;
129
- }
130
- input[type="file"]::file-selector-button {
131
- padding: 8px 15px;
132
- margin-right: 10px;
133
- background-color: #6c757d;
134
- color: white;
135
- border: none;
136
- border-radius: 7px;
137
- cursor: pointer;
138
- font-family: inherit;
139
- font-weight: 500;
140
- }
141
- input[type="file"]::file-selector-button:hover {
142
- background-color: #5a6268;
143
- }
144
-
145
- input:focus, textarea:focus {
146
- outline: none;
147
- border-color: var(--color-primary);
148
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.25);
149
- background-color: var(--color-white);
150
- }
151
- textarea {
152
- min-height: 60px;
153
- resize: vertical;
154
- }
155
- .preview-image {
156
- max-width: 100%;
157
- max-height: 150px;
158
- border-radius: var(--border-radius-md);
159
- margin-top: 12px;
160
- display: none;
161
- border: 1px solid var(--color-border);
162
- object-fit: cover;
163
- box-shadow: var(--shadow-md);
164
- }
165
-
166
- .options-grid {
167
- display: grid;
168
- grid-template-columns: 1fr 1fr;
169
- gap: 10px;
170
- }
171
- .option-button {
172
- padding: 10px;
173
- font-size: 0.85em;
174
- font-weight: 500;
175
- border: 1px solid var(--color-border);
176
- border-radius: var(--border-radius-md);
177
- background-color: var(--color-bg-light);
178
- color: var(--color-text);
179
- cursor: pointer;
180
- text-align: center;
181
- font-family: inherit;
182
- }
183
- .option-button.selected {
184
- background-color: var(--color-primary);
185
- color: var(--color-white);
186
- border-color: var(--color-primary);
187
- font-weight: 600;
188
- }
189
-
190
- .flex-row {
191
- display: flex;
192
- gap: 10px;
193
- }
194
- .flex-row .form-group {
195
- flex: 1;
196
- }
197
-
198
- .button-container {
199
- text-align: center;
200
- margin-top: 25px;
201
- }
202
- #generateButton {
203
- background: linear-gradient(45deg, var(--color-primary) 0%, var(--color-secondary) 100%);
204
- color: white;
205
- padding: 13px 28px;
206
- border: none;
207
- border-radius: var(--border-radius-md);
208
- font-size: 1em;
209
- font-weight: 600;
210
- cursor: pointer;
211
- font-family: inherit;
212
- width: 100%;
213
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
214
- }
215
- #generateButton:hover:not(:disabled) {
216
- transform: scale(1.02);
217
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
218
- }
219
- #generateButton:disabled {
220
- background: var(--color-border);
221
- color: var(--color-text-muted);
222
- cursor: not-allowed;
223
- box-shadow: none;
224
- }
225
-
226
- .status-section, .output-section {
227
- margin-top: 25px;
228
- padding: 20px;
229
- background-color: var(--color-white);
230
- border-radius: var(--border-radius-lg);
231
- border: 1px solid var(--color-border);
232
- box-shadow: var(--shadow-md);
233
- }
234
- .status-section h2, .output-section h2 {
235
- font-size: 1.15em;
236
- color: var(--color-text);
237
- border-bottom: 1px solid var(--color-border);
238
- padding-bottom: 8px;
239
- margin-bottom: 15px;
240
- font-weight: 600;
241
- }
242
- .loader {
243
- width: 30px;
244
- height: 30px;
245
- margin: 15px auto;
246
- display: none;
247
- border-radius: 50%;
248
- border: 3px solid var(--color-bg-light);
249
- border-top-color: var(--color-primary);
250
- animation: spin 0.7s linear infinite;
251
- }
252
- @keyframes spin { to { transform: rotate(360deg); } }
253
-
254
- .progress-bar-container {
255
- width: 100%;
256
- background-color: var(--color-border);
257
- border-radius: 6px;
258
- margin: 12px 0;
259
- display: none;
260
- height: 6px;
261
- }
262
- .progress-bar {
263
- width: 0%;
264
- height: 100%;
265
- background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%);
266
- border-radius: 6px;
267
- }
268
- .status-messages-container {
269
- max-height: 90px;
270
- overflow-y: auto;
271
- padding-right: 5px;
272
- }
273
- .status-message {
274
- padding: 8px 12px;
275
- border-radius: var(--border-radius-md);
276
- margin-top: 8px;
277
- font-size: 0.85em;
278
- background-color: var(--color-bg-light);
279
- }
280
- .status-message.info {
281
- border-left: 3px solid var(--color-primary);
282
- background-color: #EBF4FF;
283
- color: #0D47A1;
284
- }
285
- .status-message.error {
286
- border-left: 3px solid var(--color-error);
287
- background-color: #FFEBEE;
288
- color: #C62828;
289
- }
290
- .status-message.success {
291
- border-left: 3px solid var(--color-success);
292
- background-color: #E8F5E9;
293
- color: #1B5E20;
294
- }
295
-
296
- #outputVideo {
297
- width: 100%;
298
- border-radius: var(--border-radius-md);
299
- margin-top: 10px;
300
- border: 1px solid var(--color-border);
301
- background-color: #000;
302
- }
303
- #finalSeed {
304
- font-size: 0.8em;
305
- color: var(--color-text-muted);
306
- text-align: center;
307
- margin-top: 8px;
308
- }
309
-
310
- .hidden-section {
311
- display: none !important;
312
- }
313
-
314
- @media (max-width: 480px) {
315
- .ltx-video-creator-app {
316
- padding: 0;
317
- margin: 10px auto;
318
- }
319
- .app-header h1 {
320
- font-size: 1.6em;
321
- }
322
- .form-section-card {
323
- padding: 15px;
324
- border-radius: var(--border-radius-md);
325
- }
326
- #generateButton {
327
- padding: 12px 18px;
328
- font-size: 0.95em;
329
- }
330
- }
331
  </style>
332
- <div class="ltx-video-creator-app">
333
- <div class="app-header">
334
- <h1><img src="https://em-content.zobj.net/thumbs/120/apple/354/film-projector_1f4fd-fe0f.png" alt="🎬" /> ویدیو ساز LTX</h1>
335
- <p>انیمیشن&zwnj;های کوتاه و جذاب خلق کنید</p>
336
- </div>
337
- <div class="form-section-card">
338
- <div class="mode-selector"><button class="mode-button active" data-mode="image-to-video">از تصویر</button> <button class="mode-button" data-mode="text-to-video">از متن</button></div>
339
- <div id="imageToVideoSection" class="form-mode-section">
340
- <div class="form-group"><label for="imageFile">تصویر شما</label> <input type="file" id="imageFile" accept="image/jpeg, image/png, image/webp" /> <img id="imagePreview" src="#" alt="پیش&zwnj;نمایش" class="preview-image" /></div>
341
- </div>
342
- <div id="textToVideoSection" class="form-mode-section hidden-section">
343
- <p style="text-align: center; font-size: 0.8rem; margin-bottom: 15px; padding: 8px; background-color: var(--color-bg-light); border-radius: 7px;">در این حالت، ویدیو تنها بر اساس شرح شما ساخته می&zwnj;شود.</p>
344
- </div>
345
- <div class="form-group"><label for="prompt">شرح انیمیشن (Prompt)</label> <textarea id="prompt" rows="2" placeholder="مثال: گربه&zwnj;ای که در چمنزار می&zwnj;دود">یک موجود از داخل تصویر شروع به حرکت می‌کند</textarea></div>
346
- <div class="form-group"><label>مدت زمان</label>
347
- <div class="options-grid" id="durationButtonsContainer"><button class="option-button duration-button" data-api-duration="5">کوتاه (۵ ثانیه)</button> <button class="option-button duration-button selected" data-api-duration="7.8">استاندارد (۸ ثانیه)</button></div>
348
- </div>
349
- <div class="form-group"><label>نسبت تصویر خروجی</label>
350
- <div class="options-grid" id="aspectRatioButtonsContainer"><button class="option-button aspect-ratio-button selected" data-height="768" data-width="768">۱:۱ (مربع)</button> <button class="option-button aspect-ratio-button" data-height="768" data-width="432">۹:۱۶ (پرتره)</button> <button class="option-button aspect-ratio-button" data-height="432" data-width="768">۱۶:۹ (لند��سکیپ)</button> <button class="option-button aspect-ratio-button" data-height="512" data-width="704">LTX (۵۱۲&times;۷۰۴)</button></div>
351
- </div>
352
- <div class="form-group"><label for="negativePrompt">موارد ناخواسته (Negative Prompt)</label> <textarea id="negativePrompt" rows="2" placeholder="مثال: کیفیت پایین, متن">کیفیت پایین، تار، لرزان, متن ناخوانا</textarea></div>
353
- <div class="flex-row">
354
- <div class="form-group"><label for="cfgScale">قدرت Prompt</label> <input type="number" id="cfgScale" value="1.0" min="1.0" max="10.0" step="0.1" /></div>
355
- <div class="form-group"><label for="seed">سید (0=تصادفی)</label> <input type="number" id="seed" value="0" min="0" /></div>
356
- </div>
357
- <div class="flex-row">
358
- <div class="form-group"><label for="outputHeight">ارتفاع (px)</label> <input type="number" id="outputHeight" value="768" step="32" min="256" max="1280" /></div>
359
- <div class="form-group"><label for="outputWidth">عرض (px)</label> <input type="number" id="outputWidth" value="768" step="32" min="256" max="1280" /></div>
360
- </div>
361
- <div class="button-container"><button id="generateButton">🚀 تولید ویدیو</button></div>
362
- </div>
363
- <div class="status-section" id="statusSection" style="display: none;">
364
- <h2>وضعیت پردازش</h2>
365
- <div class="loader" id="loader"></div>
366
- <div class="progress-bar-container" id="progressBarContainer">
367
- <div class="progress-bar" id="progressBar"></div>
368
- </div>
369
- <div class="status-messages-container" id="statusMessages"></div>
370
- </div>
371
- <div class="output-section" id="outputSection" style="display: none;">
372
- <h2>ویدیوی شما آماده است!</h2>
373
- <video width="300" height="150" id="outputVideo" controls="controls" preload="metadata" playsinline=""></video>
374
- <p id="finalSeed">&nbsp;</p>
375
- </div>
376
- </div>
377
- <p>
378
- <script>
379
- const imageFileInput = document.getElementById('imageFile');
380
- const imagePreview = document.getElementById('imagePreview');
381
- const promptInput = document.getElementById('prompt');
382
- const durationButtons = document.querySelectorAll('.duration-button');
383
- const aspectRatioButtons = document.querySelectorAll('.aspect-ratio-button');
384
- const generateButton = document.getElementById('generateButton');
385
- const outputVideo = document.getElementById('outputVideo');
386
- const finalSeedElement = document.getElementById('finalSeed');
387
- const statusMessagesDiv = document.getElementById('statusMessages');
388
- const statusSection = document.getElementById('statusSection');
389
- const outputSection = document.getElementById('outputSection');
390
- const loader = document.getElementById('loader');
391
- const progressBarContainer = document.getElementById('progressBarContainer');
392
- const progressBar = document.getElementById('progressBar');
393
- const negativePromptInput = document.getElementById('negativePrompt');
394
- const cfgScaleInput = document.getElementById('cfgScale');
395
- const seedInput = document.getElementById('seed');
396
- const outputHeightInput = document.getElementById('outputHeight');
397
- const outputWidthInput = document.getElementById('outputWidth');
398
- const modeButtons = document.querySelectorAll('.mode-button');
399
- const imageToVideoSection = document.getElementById('imageToVideoSection');
400
- const textToVideoSection = document.getElementById('textToVideoSection');
401
- let currentGenerationMode = "image-to-video";
402
- const SPACE_URL_BASE = "https://lightricks-ltx-video-distilled.hf.space";
403
- let selectedApiDuration = 7.8;
404
- let currentSessionHash = '';
405
- let uploadedImageInfo = null;
406
-
407
  document.addEventListener('DOMContentLoaded', () => {
408
- document.querySelector('.duration-button.selected')?.click();
409
- document.querySelector('.aspect-ratio-button.selected')?.click();
410
- updateFormForMode(currentGenerationMode);
411
- });
412
-
413
- modeButtons.forEach(button => {
414
- button.addEventListener('click', () => {
415
- modeButtons.forEach(btn => btn.classList.remove('active'));
416
- button.classList.add('active');
417
- currentGenerationMode = button.dataset.mode;
418
- updateFormForMode(currentGenerationMode);
419
- });
420
- });
421
-
422
- function updateFormForMode(mode) {
423
- if (mode === "image-to-video") {
424
- imageToVideoSection.classList.remove('hidden-section');
425
- textToVideoSection.classList.add('hidden-section');
426
- promptInput.placeholder = "مثال: گربه‌ای در چمنزار آفتابی می‌دود";
427
- } else {
428
- imageToVideoSection.classList.add('hidden-section');
429
- textToVideoSection.classList.remove('hidden-section');
430
- imageFileInput.value = '';
431
- imagePreview.style.display = 'none';
432
- uploadedImageInfo = null;
433
- promptInput.placeholder = "مثال: یک اژدهای آتشین بر فراز قلعه‌ای قرون وسطایی";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  }
435
- }
436
-
437
- imageFileInput.addEventListener('change', function(event) {
438
- const file = event.target.files[0];
439
- if (file) {
440
- if (!file.type.startsWith('image/')) {
441
- addStatusMessage('لطفاً یک فایل تصویری انتخاب کنید.', 'error');
442
- imageFileInput.value = '';
443
- imagePreview.style.display = 'none';
444
- return;
445
- }
446
- const reader = new FileReader();
447
- reader.onload = function(e) {
448
- imagePreview.src = e.target.result;
449
- imagePreview.style.display = 'block';
450
- };
451
- reader.readAsDataURL(file);
452
- uploadedImageInfo = null;
453
- } else {
454
- imagePreview.style.display = 'none';
455
  }
456
- });
457
-
458
- durationButtons.forEach(button => {
459
- button.addEventListener('click', () => {
460
- durationButtons.forEach(btn => btn.classList.remove('selected'));
461
- button.classList.add('selected');
462
- selectedApiDuration = parseFloat(button.dataset.apiDuration);
463
- });
464
- });
465
-
466
- aspectRatioButtons.forEach(button => {
467
- button.addEventListener('click', () => {
468
- aspectRatioButtons.forEach(btn => btn.classList.remove('selected'));
469
- button.classList.add('selected');
470
- outputHeightInput.value = button.dataset.height;
471
- outputWidthInput.value = button.dataset.width;
472
- });
473
- });
474
-
475
- function addStatusMessage(message, type = 'info') {
476
- statusSection.style.display = 'block';
477
- const messageDiv = document.createElement('div');
478
- messageDiv.className = `status-message ${type}`;
479
- messageDiv.textContent = message;
480
- statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
481
- }
482
 
483
- function clearStatusAndOutput() {
484
- statusMessagesDiv.innerHTML = '';
485
- statusSection.style.display = 'none';
486
- outputSection.style.display = 'none';
487
- if (outputVideo.src) URL.revokeObjectURL(outputVideo.src);
488
- outputVideo.src = '';
489
- finalSeedElement.textContent = '';
490
- progressBar.style.width = '0%';
491
- progressBarContainer.style.display = 'none';
492
- }
493
-
494
- function generateRandomHash(length = 11) {
495
- const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
496
- let result = '';
497
- for (let i = 0; i < length; i++) {
498
- result += characters.charAt(Math.floor(Math.random() * characters.length));
499
- }
500
- return result;
501
- }
502
-
503
- async function uploadImage(file) {
504
- addStatusMessage('۱. آماده‌سازی تصویر...');
505
- const uploadId = generateRandomHash(12);
506
- const formData = new FormData();
507
- formData.append('files', file);
508
- try {
509
- const response = await fetch(`${SPACE_URL_BASE}/gradio_api/upload?upload_id=${uploadId}`, {
510
- method: 'POST',
511
- body: formData
512
- });
513
- if (!response.ok) throw new Error(`خطا در آپلود (${response.status})`);
514
- const result = await response.json();
515
- if (result && result.length > 0 && typeof result[0] === 'string') {
516
- return {
517
- path: result[0],
518
- url: `${SPACE_URL_BASE}/gradio_api/file=${result[0]}`,
519
- orig_name: file.name,
520
- size: file.size,
521
- mime_type: file.type || 'application/octet-stream',
522
- meta: { "_type": "gradio.FileData" }
523
- };
524
  }
525
- throw new Error('پاسخ آپلود نامعتبر.');
526
- } catch (error) {
527
- addStatusMessage(`خطا در آپلود: ${error.message}`, 'error');
528
- return null;
529
  }
530
- }
531
 
532
- async function submitToQueue(payload) {
533
- addStatusMessage('۲. ارسال درخواست به سرور...');
534
- try {
535
- const response = await fetch(`${SPACE_URL_BASE}/gradio_api/queue/join`, {
536
- method: 'POST',
537
- headers: { 'Content-Type': 'application/json' },
538
- body: JSON.stringify(payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  });
540
- if (!response.ok) throw new Error(`خطا در ارسال به صف (${response.status})`);
541
- return await response.json();
542
- } catch (error) {
543
- addStatusMessage(`خطا در ارتباط با سرور: ${error.message}`, 'error');
544
- return null;
545
  }
546
- }
547
-
548
- function listenForResults(eventId) {
549
- addStatusMessage('۳. در حال تولید ویدیو...');
550
- const eventSource = new EventSource(`${SPACE_URL_BASE}/gradio_api/queue/data?session_hash=${currentSessionHash}`);
551
-
552
- eventSource.onmessage = function(event) {
553
- const data = JSON.parse(event.data);
554
- if (data.event_id !== eventId && data.msg !== "queue_full") return;
555
- switch (data.msg) {
556
- case "process_starts":
557
- addStatusMessage('عملیات در سرور آغاز شد.');
558
- progressBarContainer.style.display = 'block';
559
- progressBar.style.width = '0%';
560
- break;
561
- case "progress":
562
- if (data.progress_data && data.progress_data.length > 0) {
563
- const progress = Math.round((data.progress_data[0].progress || 0) * 100);
564
- progressBar.style.width = `${progress}%`;
565
- }
566
- break;
567
- case "process_completed":
568
- eventSource.close();
569
- loader.style.display = 'none';
570
- generateButton.disabled = false;
571
- progressBar.style.width = '100%';
572
- if (data.success && data.output?.data?.[0]?.video?.url) {
573
- addStatusMessage('ویدیو با موفقیت ساخته شد! 🎉', 'success');
574
- outputSection.style.display = 'block';
575
- outputVideo.src = data.output.data[0].video.url;
576
- outputVideo.load();
577
- finalSeedElement.textContent = data.output.data[1] ? `سید نهایی: ${data.output.data[1]}` : '';
578
- } else {
579
- addStatusMessage('تولید ویدیو ناموفق بود.', 'error');
580
- }
581
- break;
582
- case "queue_full":
583
- addStatusMessage('سرور مشغول است، لطفاً کمی بعد تلاش کنید.', 'error');
584
- eventSource.close();
585
- loader.style.display = 'none';
586
- generateButton.disabled = false;
587
- break;
588
  }
589
- };
590
-
591
- eventSource.onerror = function() {
592
- addStatusMessage('خطا در ارتباط با سرور.', 'error');
593
- eventSource.close();
594
- loader.style.display = 'none';
595
- generateButton.disabled = false;
596
- };
597
- }
598
-
599
- generateButton.addEventListener('click', async () => {
600
- clearStatusAndOutput();
601
- const imageFile = imageFileInput.files[0];
602
-
603
- if (currentGenerationMode === "image-to-video" && !imageFile) {
604
- addStatusMessage('لطفاً ابتدا یک تصویر انتخاب کنید.', 'error');
605
- return;
606
- }
607
- if (!promptInput.value.trim()) {
608
- addStatusMessage('لطفاً متن راهنما (Prompt) را وارد کنید.', 'error');
609
- return;
610
- }
611
-
612
- generateButton.disabled = true;
613
- loader.style.display = 'block';
614
- current
615
-
616
- SessionHash = generateRandomHash();
617
 
618
- if (currentGenerationMode === "image-to-video") {
619
- uploadedImageInfo = await uploadImage(imageFile);
620
- if (!uploadedImageInfo) {
621
- generateButton.disabled = false;
622
- loader.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  return;
624
  }
625
- } else {
626
- uploadedImageInfo = null;
627
- }
628
-
629
- const userSeed = parseInt(seedInput.value);
630
- const generationPayload = {
631
- fn_index: 5,
632
- data: [
633
- promptInput.value,
634
- negativePromptInput.value,
635
- uploadedImageInfo,
636
- (currentGenerationMode === "image-to-video" ? null : ""),
637
- parseInt(outputHeightInput.value),
638
- parseInt(outputWidthInput.value),
639
- currentGenerationMode,
640
- selectedApiDuration,
641
- 9,
642
- (userSeed > 0) ? userSeed : Math.floor(Math.random() * (2**32 - 1)),
643
- (userSeed <= 0),
644
- parseFloat(cfgScaleInput.value),
645
- true
646
- ],
647
- session_hash: currentSessionHash
648
- };
649
 
650
- const joinResponse = await submitToQueue(generationPayload);
651
- if (joinResponse && joinResponse.event_id) {
652
- listenForResults(joinResponse.event_id);
653
- } else {
654
- addStatusMessage('ارسال درخواست به سرور ناموفق بود.', 'error');
655
- generateButton.disabled = false;
656
- loader.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  }
 
 
 
658
  });
659
  </script>
660
- </p>
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Alpha TTS - رابط کاربری هوشمند با عکس</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
9
+
10
  :root {
11
+ --app-font: 'Vazirmatn', sans-serif;
12
+ --app-header-grad-start: #1a2980;
13
+ --app-header-grad-end: #26d0ce;
14
+ --app-panel-bg: #FFFFFF;
15
+ --app-input-bg: #F8F9FA;
16
+ --app-button-bg: #5f27cd;
17
+ --app-button-hover-bg: #481e9e;
18
+ --app-main-bg: linear-gradient(170deg, #F3E8FF 0%, #E0F2FE 100%);
19
+ --app-text-primary: #2c3e50;
20
+ --app-text-secondary: #555;
21
+ --app-border-color: #E0E0E0;
22
+ --radius-card: 24px;
23
+ --radius-input: 12px;
24
+ --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.08);
25
+ --shadow-button: 0 4px 15px -2px rgba(95, 39, 205, 0.4);
26
+ --speaker-selected-glow: 0 0 15px rgba(95, 39, 205, 0.5);
27
  }
28
 
 
 
29
  body {
30
+ font-family: var(--app-font);
31
+ direction: rtl;
32
+ background: var(--app-main-bg);
33
+ color: var(--app-text-primary);
34
+ font-size: 16px;
35
+ line-height: 1.65;
36
+ margin: 0;
37
+ padding: 0;
38
+ min-height: 100vh;
39
+ -webkit-font-smoothing: antialiased;
40
+ -moz-osx-font-smoothing: grayscale;
41
+ }
42
+
43
+ .container { max-width: 800px; width: 95%; margin: 0 auto; padding-bottom: 40px; }
44
+ .app-header { padding: 3rem 1.5rem 5rem 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); }
45
+ .app-header h1 { font-size: 2.8em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }
46
+ .app-header p { font-size: 1.2em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }
47
+ .main-content { padding: 2.5rem; margin: -3.5rem auto 2rem auto; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); }
48
+ .form-group { margin-bottom: 2.5rem; }
49
+ label { display: block; font-weight: 700; color: var(--app-text-primary); font-size: 1.1em; margin-bottom: 0.8rem; }
50
+ textarea, input[type="text"] { width: 100%; padding: 1rem; border-radius: var(--radius-input); border: 2px solid var(--app-border-color); background-color: var(--app-input-bg); box-shadow: none; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: all 0.2s ease-in-out; }
51
+ textarea:focus, input[type="text"]:focus { outline: none; border-color: var(--app-button-bg); box-shadow: 0 0 0 3px rgba(95, 39, 205, 0.2); background-color: #fff; }
52
+
53
+ #selected-speaker-display { text-align: center; }
54
+ #selected-speaker-card { display: inline-flex; align-items: center; background: #fff; border-radius: 99px; padding: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); border: 2px solid var(--app-border-color); }
55
+ #selected-speaker-card img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-left: 15px; background-color: #eee; }
56
+ #selected-speaker-info h3 { margin: 0; font-size: 1.4em; }
57
+ #selected-speaker-info p { margin: 5px 0 0; color: var(--app-text-secondary); font-size: 0.9em; }
58
+ #change-speaker-btn { display: block; margin: 1rem auto 0; padding: 8px 20px; border-radius: 8px; background-color: var(--app-input-bg); border: 1px solid var(--app-border-color); cursor: pointer; font-family: var(--app-font); font-weight: 500; transition: all 0.2s ease; }
59
+ #change-speaker-btn:hover { background-color: #e9ecef; border-color: #ced4da; }
60
+
61
+ #speaker-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; }
62
+ #speaker-modal.visible { display: flex; opacity: 1; }
63
+ .modal-content { background: #fff; padding: 2rem; border-radius: var(--radius-card); width: 90%; max-width: 700px; max-height: 80vh; overflow-y: auto; transform: scale(0.95); transition: transform 0.3s ease; }
64
+ #speaker-modal.visible .modal-content { transform: scale(1); }
65
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
66
+ .modal-header h2 { margin: 0; }
67
+ .close-modal-btn { background: none; border: none; font-size: 2rem; cursor: pointer; color: #aaa; transition: color 0.2s ease; }
68
+ .close-modal-btn:hover { color: #333; }
69
+
70
+ #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; }
71
+ @media (min-width: 576px) { #speaker-grid { grid-template-columns: repeat(4, 1fr); } }
72
+ .speaker-card { cursor: pointer; transition: all 0.3s ease; }
73
+ .speaker-card .speaker-visual { border: 3px solid transparent; border-radius: var(--radius-card); overflow: hidden; text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.05); position: relative; background-color: #fff; }
74
+ .speaker-card:hover .speaker-visual { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0,0,0,0.1); }
75
+ .speaker-card input[type="radio"] { display: none; }
76
+ .speaker-card img { width: 100%; height: 120px; object-fit: cover; display: block; background-color: #eee; }
77
+ .speaker-card .speaker-name { padding: 0.7rem 0.5rem; font-weight: 500; font-size: 0.9em; }
78
+ .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--app-button-bg); box-shadow: var(--speaker-selected-glow); }
79
+
80
+ .slider-container { display: flex; align-items: center; gap: 1rem; }
81
+ input[type="range"] { flex-grow: 1; cursor: pointer; }
82
+ #temperature-value { font-weight: bold; background-color: var(--app-input-bg); padding: 0.2rem 0.8rem; border-radius: 8px; border: 1px solid var(--app-border-color); min-width: 40px; text-align: center; }
83
+ #generate-btn { width: 100%; padding: 1rem 1.5rem; font-size: 1.2em; font-weight: 700; font-family: var(--app-font); background: var(--app-button-bg); color: white; border: none; border-radius: var(--radius-input); cursor: pointer; transition: all 0.3s ease; box-shadow: var(--shadow-button); }
84
+ #generate-btn:hover:not(:disabled) { background-color: var(--app-button-hover-bg); transform: translateY(-2px); box-shadow: 0 6px 20px -3px rgba(95, 39, 205, 0.5); }
85
+ #generate-btn:disabled { background-color: #999; cursor: not-allowed; box-shadow: none; }
86
+ #output-section { margin-top: 2.5rem; padding: 1.5rem; background-color: #fff; border-radius: var(--radius-card); min-height: 100px; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; border: 1px solid var(--app-border-color); }
87
+ #status-message { font-weight: 500; color: var(--app-text-secondary); }
88
+ #audio-player { width: 100%; margin-top: 1rem; display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </style>
90
+ </head>
91
+ <body>
92
+ <div class="container">
93
+ <header class="app-header">
94
+ <h1>آلفا TTS</h1>
95
+ <p>جادوی تبدیل متن به صدا، به سبک شما</p>
96
+ </header>
97
+
98
+ <main class="main-content">
99
+ <form id="tts-form">
100
+ <div class="form-group">
101
+ <label for="text-input">📝 متن برای تبدیل</label>
102
+ <textarea id="text-input" rows="5" placeholder="اینجا متن خود را به فارسی وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
103
+ </div>
104
+ <div class="form-group">
105
+ <label for="prompt-input">🗣️ سبک و لحن گفتار (اختیاری)</label>
106
+ <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی شاد و پرانرژی">
107
+ </div>
108
+
109
+ <div class="form-group">
110
+ <label>🎤 گوینده منتخب</label>
111
+ <div id="selected-speaker-display">
112
+ <div id="selected-speaker-card">
113
+ <img id="selected-speaker-img" src="" alt="عکس ��وینده">
114
+ <div id="selected-speaker-info">
115
+ <h3 id="selected-speaker-name"></h3>
116
+ <p>برای تغییر، روی دکمه زیر کلیک کنید</p>
117
+ </div>
118
+ </div>
119
+ <button type="button" id="change-speaker-btn">تغییر گوینده</button>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="form-group">
124
+ <label for="temperature-slider">🌡️ میزان خلاقیت صدا (0.1 تا 1.5)</label>
125
+ <div class="slider-container">
126
+ <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
127
+ <span id="temperature-value">0.9</span>
128
+ </div>
129
+ </div>
130
+
131
+ <button type="submit" id="generate-btn">🚀 تولید و پخش صدا</button>
132
+ </form>
133
+
134
+ <div id="output-section">
135
+ <div id="status-message">خروجی صدا در اینجا نمایش داده می‌شود</div>
136
+ <audio id="audio-player" controls></audio>
137
+ </div>
138
+ </main>
139
+ </div>
140
+
141
+ <div id="speaker-modal">
142
+ <div class="modal-content">
143
+ <div class="modal-header">
144
+ <h2>انتخاب گوینده</h2>
145
+ <button type="button" class="close-modal-btn">×</button>
146
+ </div>
147
+ <div id="speaker-grid"></div>
148
+ </div>
149
+ </div>
150
+
151
+ <input type="hidden" id="selected_speaker_id_storage" value="Charon"> <!-- مقدار پیش‌فرض از کد پایتون -->
152
+
153
+ <script>
 
 
 
 
 
 
 
 
 
 
 
154
  document.addEventListener('DOMContentLoaded', () => {
155
+ const HF_SPACE_URL = "https://hamed744-ttspro.hf.space";
156
+ const JOIN_QUEUE_URL = `${HF_SPACE_URL}/gradio_api/queue/join`;
157
+ const GET_DATA_URL_BASE = `${HF_SPACE_URL}/gradio_api/queue/data`;
158
+ const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`;
159
+ const FN_INDEX = 1;
160
+
161
+ // --- لیست گویندگان با اسامی فارسی و ID انگلیسی مطابق با کد پایتون ---
162
+ const speakers = [
163
+ { id: "Achird", name: "آرش (مرد)" },
164
+ { id: "Zubenelgenubi", name: "زهره (زن)" },
165
+ { id: "Vindemiatrix", name: "ویدا (زن)" },
166
+ { id: "Sadachbia", name: "سارا (زن)" },
167
+ { id: "Sadaltager", name: "سامان (مرد)" },
168
+ { id: "Sulafat", name: "سولماز (زن)" },
169
+ { id: "Laomedeia", name: "لیلا (زن)" },
170
+ { id: "Achernar", name: "آرمان (مرد)" },
171
+ { id: "Alnilam", name: "آیدا (زن)" },
172
+ { id: "Schedar", name: "شهاب (مرد)" },
173
+ { id: "Gacrux", name: "کاوه (مرد)" },
174
+ { id: "Pulcherrima", name: "پریا (زن)" },
175
+ { id: "Umbriel", name: "امید (مرد)" },
176
+ { id: "Algieba", name: "آزاده (زن)" },
177
+ { id: "Despina", name: "دینا (زن)" },
178
+ { id: "Erinome", name: "ایرج (مرد)" },
179
+ { id: "Algenib", name: "آرشام (مرد)" },
180
+ { id: "Rasalthgeti", name: "رها (زن)" },
181
+ { id: "Orus", name: "اردلان (مرد)" },
182
+ { id: "Aoede", name: "آیدین (؟)" }, // نام خنثی
183
+ { id: "Callirrhoe", name: "کیمیا (زن)" },
184
+ { id: "Autonoe", name: "آتنا (زن)" },
185
+ { id: "Enceladus", name: "انوش (مرد)" },
186
+ { id: "Iapetus", name: "یاشار (مرد)" },
187
+ { id: "Zephyr", name: "نسیم (زن)" }, // در مثال Gradio زنانه بود
188
+ { id: "Puck", name: "پویا (مرد)" },
189
+ { id: "Charon", name: "کارن (مرد)" }, // پیش‌فرض در Gradio شما
190
+ { id: "Kore", name: "کوروش (مرد)" },
191
+ { id: "Fenrir", name: "فرید (مرد)" },
192
+ { id: "Leda", name: "لیدا (زن)" }
193
+ ];
194
+
195
+ const form = document.getElementById('tts-form');
196
+ const textInput = document.getElementById('text-input');
197
+ const promptInput = document.getElementById('prompt-input');
198
+ const tempSlider = document.getElementById('temperature-slider');
199
+ const tempValueSpan = document.getElementById('temperature-value');
200
+ const generateBtn = document.getElementById('generate-btn');
201
+ const statusMessage = document.getElementById('status-message');
202
+ const audioPlayer = document.getElementById('audio-player');
203
+
204
+ const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
205
+ const speakerModal = document.getElementById('speaker-modal');
206
+ const changeSpeakerBtn = document.getElementById('change-speaker-btn');
207
+ const closeModalBtn = document.querySelector('.close-modal-btn');
208
+ const speakerGridInModal = document.getElementById('speaker-grid');
209
+ const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
210
+ const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
211
+
212
+ function getSpeakerById(id) {
213
+ return speakers.find(s => s.id === id);
214
  }
215
+
216
+ function getImageUrl(speaker, index) {
217
+ const genderHint = speaker.name.toLowerCase();
218
+ let gender = 'lego'; // پیشفرض برای موارد نامشخص
219
+ if (genderHint.includes('(مرد)')) gender = 'men';
220
+ else if (genderHint.includes('(زن)')) gender = 'women';
221
+
222
+ const imageIndex = (index * 5 + (speaker.id.length % 10)) % 100; // فرمول کمی پیچیده‌تر برای تنوع بیشتر
223
+ return `https://randomuser.me/api/portraits/${gender}/${imageIndex}.jpg`;
 
 
 
 
 
 
 
 
 
 
 
224
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ function updateSelectedSpeakerDisplay(speakerId) {
227
+ const speaker = getSpeakerById(speakerId);
228
+ if (speaker) {
229
+ const speakerIndex = speakers.findIndex(s => s.id === speakerId);
230
+ selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex);
231
+ selectedSpeakerNameDisplay.textContent = speaker.name;
232
+ selectedSpeakerIdStorage.value = speaker.id;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  }
 
 
 
 
234
  }
 
235
 
236
+ function createSpeakerCardsInModal() {
237
+ speakerGridInModal.innerHTML = '';
238
+ speakers.forEach((speaker, index) => {
239
+ const card = document.createElement('label');
240
+ card.className = 'speaker-card';
241
+ card.setAttribute('for', `modal-speaker-${speaker.id}`);
242
+
243
+ const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
244
+
245
+ card.innerHTML = `
246
+ <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
247
+ <div class="speaker-visual">
248
+ <img src="${getImageUrl(speaker, index)}" alt="عکس گوینده ${speaker.name}" loading="lazy">
249
+ <div class="speaker-name">${speaker.name}</div>
250
+ </div>
251
+ `;
252
+
253
+ card.addEventListener('click', () => {
254
+ updateSelectedSpeakerDisplay(speaker.id);
255
+ setTimeout(() => speakerModal.classList.remove('visible'), 100);
256
+ });
257
+
258
+ speakerGridInModal.appendChild(card);
259
  });
 
 
 
 
 
260
  }
261
+
262
+ changeSpeakerBtn.addEventListener('click', () => {
263
+ createSpeakerCardsInModal();
264
+ speakerModal.classList.add('visible');
265
+ });
266
+ closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
267
+ speakerModal.addEventListener('click', (e) => {
268
+ if (e.target === speakerModal) {
269
+ speakerModal.classList.remove('visible');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
+ tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
274
+
275
+ async function generateAudio(event) {
276
+ event.preventDefault();
277
+ generateBtn.disabled = true;
278
+ generateBtn.textContent = 'در حال پردازش...';
279
+ statusMessage.textContent = 'در حال ارسال درخواست به سرور...';
280
+ audioPlayer.style.display = 'none';
281
+ audioPlayer.src = '';
282
+
283
+ const text = textInput.value;
284
+ const prompt = promptInput.value;
285
+ const temperature = parseFloat(tempSlider.value);
286
+ const selectedSpeaker = selectedSpeakerIdStorage.value;
287
+ const sessionHash = Math.random().toString(36).substring(2);
288
+
289
+ if (!text.trim()) {
290
+ statusMessage.textContent = 'خطا: متن ورودی نمی‌تواند خالی باشد.';
291
+ generateBtn.disabled = false;
292
+ generateBtn.textContent = '🚀 تولید و پخش صدا';
293
  return;
294
  }
295
+
296
+ const payload = {
297
+ fn_index: FN_INDEX,
298
+ data: [false, null, text, prompt, selectedSpeaker, temperature],
299
+ event_data: null,
300
+ session_hash: sessionHash
301
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ try {
304
+ const joinQueueResponse = await fetch(JOIN_QUEUE_URL, {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json" },
307
+ body: JSON.stringify(payload)
308
+ });
309
+
310
+ if (!joinQueueResponse.ok) {
311
+ const errorBody = await joinQueueResponse.text();
312
+ throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status}): ${errorBody}`);
313
+ }
314
+
315
+ statusMessage.textContent = 'در انتظار نتیجه از سرور...';
316
+
317
+ const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
318
+ const reader = dataResponse.body.getReader();
319
+ const decoder = new TextDecoder();
320
+ let finalFilePath = null;
321
+ let buffer = '';
322
+
323
+ while (true) {
324
+ const { value, done } = await reader.read();
325
+ if (done) break;
326
+
327
+ buffer += decoder.decode(value, { stream: true });
328
+ const lines = buffer.split('\n');
329
+ buffer = lines.pop();
330
+
331
+ for (const line of lines) {
332
+ if (!line.startsWith('data:')) continue;
333
+
334
+ try {
335
+ const data = JSON.parse(line.substring(5));
336
+ if (data.msg === 'process_generating') {
337
+ statusMessage.textContent = 'سرور در حال تولید فایل صوتی است...';
338
+ }
339
+ if (data.msg === 'process_completed') {
340
+ if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
341
+ finalFilePath = data.output.data[0].name || data.output.data[0].path;
342
+ } else {
343
+ console.error("ساختار پیام موفقیت مورد انتظار نبود:", data);
344
+ }
345
+ break;
346
+ }
347
+ } catch (e) { /* نادیده گرفتن خطاهای پارس */ }
348
+ }
349
+ if (finalFilePath) break;
350
+ }
351
+
352
+ if (finalFilePath) {
353
+ statusMessage.textContent = 'فایل صوتی با موفقیت دریافت شد!';
354
+ const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
355
+ audioPlayer.src = audioUrl;
356
+ audioPlayer.style.display = 'block';
357
+ audioPlayer.play();
358
+ } else {
359
+ throw new Error('فایل صوتی از سرور دریافت نشد. کنسول را برای اطلاعات بیشتر بررسی کنید.');
360
+ }
361
+
362
+ } catch (error) {
363
+ console.error('یک خطا در فرآیند رخ داد:', error);
364
+ statusMessage.textContent = `یک خطا رخ داد: ${error.message}`;
365
+ } finally {
366
+ generateBtn.disabled = false;
367
+ generateBtn.textContent = '🚀 تولید و پخش صدا';
368
+ }
369
  }
370
+
371
+ updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value);
372
+ form.addEventListener('submit', generateAudio);
373
  });
374
  </script>
375
+ </body>
376
+ </html>