Opera10 commited on
Commit
e512984
·
verified ·
1 Parent(s): 84d5f7c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +315 -515
index.html CHANGED
@@ -1,531 +1,331 @@
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>کلاینت API انیماتور Wan 2.2</title>
7
- <style>
8
- :root {
9
- --bg-color: #0b0f19;
10
- --card-bg: #151d30;
11
- --primary: #6366f1;
12
- --primary-hover: #4f46e5;
13
- --text-color: #f8fafc;
14
- --text-muted: #94a3b8;
15
- --border-color: #1e293b;
16
- --success: #22c55e;
17
- --warning: #f59e0b;
18
- }
19
-
20
- * {
21
- box-sizing: border-box;
22
- margin: 0;
23
- padding: 0;
24
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
25
- }
26
-
27
- body {
28
- background-color: var(--bg-color);
29
- color: var(--text-color);
30
- padding: 2rem 1rem;
31
- line-height: 1.6;
32
- }
33
-
34
- .container {
35
- max-width: 1200px;
36
- margin: 0 auto;
37
- }
38
-
39
- header {
40
- text-align: center;
41
- margin-bottom: 2.5rem;
42
- }
43
-
44
- header h1 {
45
- font-size: 2.2rem;
46
- color: var(--text-color);
47
- margin-bottom: 0.5rem;
48
- font-weight: 800;
49
- }
50
-
51
- header p {
52
- color: var(--text-muted);
53
- font-size: 1rem;
54
- }
55
-
56
- .main-grid {
57
- display: grid;
58
- grid-template-columns: 1fr;
59
- gap: 2rem;
60
- }
61
-
62
- @media (min-width: 768px) {
63
- .main-grid {
64
- grid-template-columns: 1fr 1fr;
65
- }
66
- }
67
-
68
- .card {
69
- background-color: var(--card-bg);
70
- border: 1px solid var(--border-color);
71
- border-radius: 12px;
72
- padding: 1.5rem;
73
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
74
- }
75
-
76
- .card h2 {
77
- font-size: 1.3rem;
78
- margin-bottom: 1.5rem;
79
- border-bottom: 2px solid var(--border-color);
80
- padding-bottom: 0.5rem;
81
- color: var(--text-color);
82
- }
83
-
84
- .form-group {
85
- margin-bottom: 1.25rem;
86
- }
87
-
88
- .form-group label {
89
- display: block;
90
- margin-bottom: 0.5rem;
91
- font-weight: 600;
92
- color: var(--text-color);
93
- font-size: 0.9rem;
94
- }
95
-
96
- .form-group input[type="file"],
97
- .form-group input[type="text"],
98
- .form-group select {
99
- width: 100%;
100
- padding: 0.75rem;
101
- background-color: #0f172a;
102
- border: 1px solid var(--border-color);
103
- border-radius: 8px;
104
- color: var(--text-color);
105
- font-size: 0.9rem;
106
- outline: none;
107
- transition: border-color 0.2s;
108
- }
109
-
110
- .form-group input:focus,
111
- .form-group select:focus {
112
- border-color: var(--primary);
113
- }
114
-
115
- .form-group small {
116
- display: block;
117
- margin-top: 0.25rem;
118
- color: var(--text-muted);
119
- font-size: 0.8rem;
120
- }
121
-
122
- .btn {
123
- display: block;
124
- width: 100%;
125
- padding: 1rem;
126
- background-color: var(--primary);
127
- color: white;
128
- border: none;
129
- border-radius: 8px;
130
- font-size: 1rem;
131
- font-weight: 700;
132
- cursor: pointer;
133
- transition: background-color 0.2s, transform 0.1s;
134
- }
135
-
136
- .btn:hover {
137
- background-color: var(--primary-hover);
138
- }
139
-
140
- .btn:active {
141
- transform: scale(0.98);
142
- }
143
-
144
- .btn:disabled {
145
- background-color: var(--border-color);
146
- color: var(--text-muted);
147
- cursor: not-allowed;
148
- }
149
-
150
- /* Status panel */
151
- .status-panel {
152
- background-color: #1e1b4b;
153
- border: 1px solid #312e81;
154
- border-radius: 8px;
155
- padding: 1rem;
156
- margin-top: 1.5rem;
157
- display: none;
158
- }
159
-
160
- .status-header {
161
- font-weight: bold;
162
- margin-bottom: 0.5rem;
163
- color: #c7d2fe;
164
- }
165
-
166
- .progress-bar-container {
167
- background-color: #312e81;
168
- height: 8px;
169
- border-radius: 4px;
170
- overflow: hidden;
171
- margin-top: 0.5rem;
172
- }
173
-
174
- .progress-bar {
175
- background-color: var(--primary);
176
- height: 100%;
177
- width: 0%;
178
- transition: width 0.3s;
179
- }
180
-
181
- .queue-info {
182
- font-size: 0.85rem;
183
- color: #a5b4fc;
184
- margin-top: 0.25rem;
185
- }
186
-
187
- /* Result Section */
188
- .results-section {
189
- display: none;
190
- grid-column: 1 / -1;
191
- margin-top: 2rem;
192
- }
193
-
194
- .results-section h2 {
195
- margin-bottom: 1rem;
196
- }
197
 
198
- .video-grid {
199
- display: grid;
200
- grid-template-columns: 1fr;
201
- gap: 1.5rem;
202
- }
203
 
204
- @media (min-width: 768px) {
205
- .video-grid {
206
- grid-template-columns: 1fr 1fr;
207
- }
208
- }
209
 
210
- .video-card {
211
- background-color: var(--card-bg);
212
- border: 1px solid var(--border-color);
213
- border-radius: 12px;
214
- padding: 1rem;
215
- display: flex;
216
- flex-direction: column;
217
- }
218
 
219
- .video-card h3 {
220
- font-size: 1rem;
221
- margin-bottom: 0.75rem;
222
- color: var(--text-muted);
223
- }
224
 
225
- .video-card video {
226
- width: 100%;
227
- border-radius: 8px;
228
- background-color: #000;
229
- }
230
 
231
- .state-download-btn {
232
- display: none;
233
- margin-top: 1rem;
234
- padding: 0.75rem;
235
- background-color: var(--warning);
236
- color: #000;
237
- text-align: center;
238
- text-decoration: none;
239
- font-weight: bold;
240
- border-radius: 8px;
241
- font-size: 0.9rem;
242
- }
243
 
244
- .state-download-btn:hover {
245
- opacity: 0.9;
246
- }
247
- </style>
248
- </head>
249
- <body>
250
 
251
- <div class="container">
252
- <header>
253
- <h1>کلاینت API انیماتور Wan 2.2 🦆</h1>
254
- <p>تولید انیمیشن و تعویض کاراکتر مبتنی بر هوش مصنوعی - اتصال به سرور ZeroGPU هافینگ فیس</p>
255
- </header>
256
-
257
- <div class="main-grid">
258
- <!-- پنل تنظیمات ورودی -->
259
- <div class="card">
260
- <h2>تنظیمات انیمیشن</h2>
261
-
262
- <!-- توکن هافینگ فیس -->
263
- <div class="form-group">
264
- <label for="hf-token">توکن هافینگ فیس (HF Token - اختیاری)</label>
265
- <input type="text" id="hf-token" placeholder="hf_..." />
266
- <small>وارد کردن توکن اختیاری است، اما به پایداری درخواست‌ها و رهایی از محدودیت نرخ تماس (Rate Limit) کمک می‌کند.</small>
267
- </div>
268
-
269
- <!-- ویدیوی مرجع -->
270
- <div class="form-group">
271
- <label for="video-input">۱. ویدیوی مرجع ورودی (Video)</label>
272
- <input type="file" id="video-input" accept="video/*" />
273
- </div>
274
-
275
- <!-- تصویر مرجع -->
276
- <div class="form-group">
277
- <label for="image-input">۲. تصویر هدف ورودی (Ref Image)</label>
278
- <input type="file" id="image-input" accept="image/*" />
279
- </div>
280
-
281
- <!-- حالت اجرا -->
282
- <div class="form-group">
283
- <label for="mode-select">۳. نوع پردازش (Mode)</label>
284
- <select id="mode-select">
285
- <option value="Character Swap">تعویض کاراکتر (Character Swap)</option>
286
- <option value="Pose Retarget">انتقال فیگور/پوز (Pose Retarget)</option>
287
- </select>
288
- </div>
289
-
290
- <!-- رزولوشن -->
291
- <div class="form-group">
292
- <label for="resolution-select">۴. رزولوشن خروجی (Resolution)</label>
293
- <select id="resolution-select">
294
- <option value="Medium Res" selected>کیفیت متوسط (Medium Res)</option>
295
- <option value="High Res">کیفیت بالا (High Res)</option>
296
- </select>
297
- <small>کیفیت بالا نیاز به زمان پردازش بیشتری دارد.</small>
298
- </div>
299
-
300
- <button id="process-btn" class="btn">تولید انیمیشن 🦆</button>
301
-
302
- <!-- پنل مانیتور وضعیت زنده -->
303
- <div id="status-panel" class="status-panel">
304
- <div id="status-header" class="status-header">در انتظار برقراری ارتباط...</div>
305
- <div id="status-desc" class="status-desc">سیستم در حال آماده‌سازی فایل‌ها برای ارسال به سرور است.</div>
306
- <div id="queue-info" class="queue-info"></div>
307
- <div class="progress-bar-container">
308
- <div id="progress-bar" class="progress-bar"></div>
309
- </div>
310
- </div>
311
- </div>
312
-
313
- <!-- پیش‌نمایش خروجی اصلی -->
314
- <div class="card" style="display: flex; flex-direction: column; justify-content: space-between;">
315
- <div>
316
- <h2>خروجی نهایی</h2>
317
- <p style="color: var(--text-muted); margin-bottom: 1rem; font-size: 0.9rem;">پس از پایان موفق پردازش توسط سرور، خروجی نهایی در این بخش پخش خواهد شد.</p>
318
- </div>
319
- <div style="background-color: #0f172a; border-radius: 8px; height: 350px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
320
- <video id="final-video" style="width: 100%; height: 100%; display: none;" controls></video>
321
- <div id="video-placeholder" style="color: var(--text-muted); text-align: center;">
322
- <p style="font-size: 2.5rem; margin-bottom: 0.5rem;">🎬</p>
323
- <p>در انتظار اجرای پردازش...</p>
324
- </div>
325
- </div>
326
- <a href="#" id="state-file-download" class="state-download-btn" download>دانلود فایل وضعیت جهت ادامه</a>
327
- </div>
328
-
329
- <!-- پنل ویدیوهای فرعی و فنی خروجی (آنالیزها) -->
330
- <div id="results-section" class="results-section">
331
- <h2>مراحل پیش‌پردازش فنی (Intermediate Channels)</h2>
332
- <div class="video-grid">
333
- <div class="video-card">
334
- <h3>ویدیو استخراج اسکلت و پوز (Pose Channel)</h3>
335
- <video id="pose-video" controls></video>
336
- </div>
337
- <div class="video-card">
338
- <h3>پس‌زمینه جداسازی شده (Background)</h3>
339
- <video id="bg-video" controls></video>
340
- </div>
341
- <div class="video-card">
342
- <h3>ماسک تفکیک کاراکتر (Mask)</h3>
343
- <video id="mask-video" controls></video>
344
- </div>
345
- <div class="video-card">
346
- <h3>آنالیز و فریم‌های چهره (Face)</h3>
347
- <video id="face-video" controls></video>
348
- </div>
349
- </div>
350
- </div>
351
- </div>
352
  </div>
353
 
354
- <!-- ایمپورت کلاینت رسمی Gradio JS به عنوان ماژول ES از CDN معتبر -->
355
- <script type="module">
356
- import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
357
-
358
- const processBtn = document.getElementById("process-btn");
359
- const statusPanel = document.getElementById("status-panel");
360
- const statusHeader = document.getElementById("status-header");
361
- const statusDesc = document.getElementById("status-desc");
362
- const queueInfo = document.getElementById("queue-info");
363
- const progressBar = document.getElementById("progress-bar");
364
- const finalVideo = document.getElementById("final-video");
365
- const videoPlaceholder = document.getElementById("video-placeholder");
366
-
367
- // المان‌های بخش پردازش‌های فنی
368
- const resultsSection = document.getElementById("results-section");
369
- const poseVideo = document.getElementById("pose-video");
370
- const bgVideo = document.getElementById("bg-video");
371
- const maskVideo = document.getElementById("mask-video");
372
- const faceVideo = document.getElementById("face-video");
373
- const stateFileDownload = document.getElementById("state-file-download");
374
-
375
- processBtn.addEventListener("click", async () => {
376
- const videoFile = document.getElementById("video-input").files[0];
377
- const imageFile = document.getElementById("image-input").files[0];
378
- const hfToken = document.getElementById("hf-token").value.trim();
379
- const mode = document.getElementById("mode-select").value;
380
- const resolution = document.getElementById("resolution-select").value;
381
-
382
- // صحت‌سنجی فیلدهای ورودی قبل از شروع فرآیند
383
- if (!videoFile || !imageFile) {
384
- alert("لطفاً فایل ویدیو ورودی و تصویر مرجع را انتخاب کنید.");
385
- return;
386
- }
387
-
388
- // غیرفعال کردن دکمه جهت جلوگیری از کلیک مجدد و راه‌اندازی مانیتورینگ وضعیت
389
- processBtn.disabled = true;
390
- processBtn.textContent = "در حال پردازش...";
391
-
392
- statusPanel.style.display = "block";
393
- statusHeader.textContent = "در حال اتصال به سرور...";
394
- statusDesc.textContent = "برقراری ارتباط با سرویس پردازش ابری Gradio... لطفاً شکیبا باشید.";
395
- queueInfo.textContent = "";
396
- progressBar.style.width = "5%";
397
- progressBar.style.backgroundColor = "var(--primary)";
398
-
399
- // مخفی‌سازی خروجی‌های قبلی در اجرای جدید
400
- finalVideo.style.display = "none";
401
- videoPlaceholder.style.display = "block";
402
- resultsSection.style.display = "none";
403
- stateFileDownload.style.display = "none";
404
-
405
- try {
406
- const clientOptions = {};
407
- if (hfToken) {
408
- clientOptions.token = hfToken; // اعمال توکن شخصی در صورت درج
409
- }
410
-
411
- // اتصال کلاینت به اسپیس هدف
412
- const client = await Client.connect("Opera8/Wan2.2-Animate-ZEROGPU", clientOptions);
413
-
414
- statusHeader.textContent = "آپلود و آماده‌سازی فایل‌ها...";
415
- statusDesc.textContent = "در حال انتقال فایل‌های ویدیویی و تصویری به فضاهای ابری هافینگ‌فیس...";
416
- progressBar.style.width = "20%";
417
-
418
- // بسته‌بندی فایل‌ها در ساختار مورد نیاز کلاینت Gradio
419
- const videoPayload = handle_file(videoFile);
420
- const imagePayload = handle_file(imageFile);
421
-
422
- // مقدار اولیه فرضی برای State بر اساس ساختار تعریف شده در فایل Python
423
- const defaultPromptState = {
424
- selections: {},
425
- active_frame: 0,
426
- cur_label_val: 1.0
427
- };
428
-
429
- // فراخوانی سرویس با متد آسنکرون submit برای کنترل زنده رویدادهای سرور
430
- // این تابع به ایندکس ۰ که تابع اصلی animate_scene است اشاره دارد.
431
- const job = client.submit(0, [
432
- videoPayload,
433
- imagePayload,
434
- mode,
435
- resolution,
436
- defaultPromptState,
437
- null // فایل وضعیت ورودی (در اولین اجرا مقدار تهی ارسال می‌شود)
438
- ]);
439
-
440
- // مانیتور زنده تغییر وضعیت صف پردازش
441
- job.on("status", (status) => {
442
- const stage = status.stage;
443
- const position = status.position;
444
- const progressData = status.progress_data ? status.progress_data[0] : null;
445
-
446
- if (stage === "pending") {
447
- statusHeader.textContent = "در صف انتظار...";
448
- queueInfo.textContent = position !== undefined ? `تعداد افراد جلوتر در صف: ${position}` : "در حال دریافت نوبت پردازش...";
449
- progressBar.style.width = "30%";
450
- } else if (stage === "running") {
451
- queueInfo.textContent = "";
452
- if (progressData) {
453
- const percent = Math.round((progressData.index / progressData.length) * 100) || 50;
454
- statusHeader.textContent = `در حال رندر و تولید فریم‌ها (${progressData.index}/${progressData.length})`;
455
- statusDesc.textContent = `سرور ابری در حال شبیه‌سازی فریم‌های ویدیویی به کمک پردازشگر گرافیکی است.`;
456
- progressBar.style.width = `${40 + (percent * 0.5)}%`;
457
- } else {
458
- statusHeader.textContent = "پردازش آغاز گردید...";
459
- statusDesc.textContent = "مدل Wan 2.2 بر روی کارت گرافیک مستقر شده و در حال تحلیل حرکتی صحنه است.";
460
- progressBar.style.width = "50%";
461
- }
462
- }
463
- });
464
-
465
- // دریافت زنده اطلاعات پردازش شده
466
- job.on("data", (event) => {
467
- const data = event.data;
468
- console.log("دریافت داده‌های خروجی سرور:", data);
469
-
470
- // بازخوانی خروجی‌های آرایه بر اساس ساختار Python:
471
- // outputs = [output_video, pose_video, bg_video, mask_video, face_video, tracking_id_out, status_out]
472
- const finalVideoObj = data[0];
473
- const poseVideoObj = data[1];
474
- const bgVideoObj = data[2];
475
- const maskVideoObj = data[3];
476
- const faceVideoObj = data[4];
477
- const stateFileObj = data[5];
478
- const statusMarkdown = data[6];
479
-
480
- // فعال‌سازی پنل پیش‌نمایش ویدیوهای کمکی پردازش شده
481
- resultsSection.style.display = "block";
482
-
483
- // در صورت آماده شدن ویدیوی نهایی
484
- if (finalVideoObj && finalVideoObj.url) {
485
- finalVideo.src = finalVideoObj.url;
486
- finalVideo.style.display = "block";
487
- videoPlaceholder.style.display = "none";
488
-
489
- statusHeader.textContent = "پردازش با موفقیت انجام شد!";
490
- statusDesc.textContent = "انیمیشن تولید شده را می‌توانید در بخش خروجی مشاهده و دانلود نمایید.";
491
- progressBar.style.width = "100%";
492
- progressBar.style.backgroundColor = "var(--success)";
493
- }
494
-
495
- // به‌روزرسانی سایر ویدیوهای کمکی
496
- if (poseVideoObj && poseVideoObj.url) poseVideo.src = poseVideoObj.url;
497
- if (bgVideoObj && bgVideoObj.url) bgVideo.src = bgVideoObj.url;
498
- if (maskVideoObj && maskVideoObj.url) maskVideo.src = maskVideoObj.url;
499
- if (faceVideoObj && faceVideoObj.url) faceVideo.src = faceVideoObj.url;
500
-
501
- // مدیریت سناریوی Timeout (تایم‌اوت): سرورهای ZeroGPU گاهی به دلیل محدودیت ۹۰ ثانیه‌ای موقتاً قطع می‌شوند.
502
- // در این حالت، فایل وضعیت صادر شده توسط سیستم در خروجی ۶ نمایش داده خواهد شد تا بتوان در تلاش بعدی رندر را ادامه داد.
503
- if (stateFileObj && stateFileObj.url) {
504
- stateFileDownload.href = stateFileObj.url;
505
- stateFileDownload.style.display = "block";
506
- statusHeader.textContent = "توقف موقت پردازش (Timeout)";
507
- statusDesc.innerHTML = `⚠️ محدودیت زمانی GPU به پایان رسید. رندر متوقف شد، اما اطلاعات تا فاز فعلی نجات یافتند. جهت تداوم فرآیند، فایل وضعیت صادر شده را دانلود و مجدداً بارگذاری کنید.<br><small>${statusMarkdown || ""}</small>`;
508
- progressBar.style.width = "80%";
509
- progressBar.style.backgroundColor = "var(--warning)";
510
- }
511
- });
512
-
513
- // انتظار برای دریافت نتیجه نهایی از اجرای تسک
514
- await job;
515
- processBtn.textContent = "تولید مجدد انیمیشن 🦆";
516
- processBtn.disabled = false;
517
-
518
- } catch (error) {
519
- console.error("خطای رخ داده:", error);
520
- statusHeader.textContent = "خطا در برقراری ارتباط با سرور";
521
- statusDesc.textContent = error.message || "بروز خطا غیرمنتظره در حین اجرای کلاینت Gradio.";
522
- progressBar.style.width = "100%";
523
- progressBar.style.backgroundColor = "#ef4444";
524
-
525
- processBtn.textContent = "تلاش مجدد 🦆";
526
- processBtn.disabled = false;
527
- }
528
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  </script>
530
 
531
  </body>
 
1
  <!DOCTYPE html>
2
  <html lang="fa" dir="rtl">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <title>Wan2.2-Animate API Client</title>
6
+ <style>
7
+ body { font-family: sans-serif; max-width: 720px; margin: 30px auto; padding: 0 16px; background:#111; color:#eee; }
8
+ h1 { font-size: 20px; }
9
+ label { display:block; margin-top: 16px; font-weight: bold; }
10
+ input, select { margin-top: 6px; padding: 8px; width: 100%; box-sizing: border-box; background:#222; color:#eee; border:1px solid #444; border-radius:6px; }
11
+ button { margin-top: 20px; padding: 12px 20px; font-size: 15px; border:none; border-radius: 8px; background: linear-gradient(45deg,#ff416c,#ff9b00); color:#fff; cursor:pointer; }
12
+ button:disabled { opacity: .5; cursor:not-allowed; }
13
+ #log { margin-top: 20px; white-space: pre-wrap; background:#000; padding:12px; border-radius:8px; font-size:13px; max-height:300px; overflow:auto; }
14
+ video { margin-top: 16px; max-width: 100%; border-radius: 8px; }
15
+
16
+ #statusBar {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 10px;
20
+ margin-top: 18px;
21
+ padding: 12px 14px;
22
+ border-radius: 10px;
23
+ background: #1a1a1a;
24
+ border: 1px solid #333;
25
+ font-size: 14px;
26
+ }
27
+ #statusDot {
28
+ width: 12px; height: 12px; border-radius: 50%;
29
+ background: #555;
30
+ flex-shrink: 0;
31
+ }
32
+ #statusDot.working { background: #ffb300; animation: pulse 1s infinite ease-in-out; }
33
+ #statusDot.done { background: #4caf50; animation: none; }
34
+ #statusDot.error { background: #f44336; animation: none; }
35
+ @keyframes pulse { 0%,100%{ opacity:1; } 50%{ opacity:.35; } }
36
+ #statusText { flex: 1; }
37
+ #statusTimer { font-variant-numeric: tabular-nums; color:#aaa; font-size: 13px; }
38
+ </style>
39
+ </head>
40
+ <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ <h1>کلاینت API عمومی Wan2.2-Animate (Gradio)</h1>
 
 
 
 
43
 
44
+ <label>توکن Hugging Face (اختیاری، برای Space های private یا محدودیت کمتر)</label>
45
+ <input id="hfToken" type="password" placeholder="hf_xxxxxxxxxxxx">
 
 
 
46
 
47
+ <label>ویدیوی ورودی</label>
48
+ <input id="videoFile" type="file" accept="video/*">
 
 
 
 
 
 
49
 
50
+ <label>تصویر مرجع (Ref Image)</label>
51
+ <input id="imageFile" type="file" accept="image/*">
 
 
 
52
 
53
+ <label>حالت</label>
54
+ <select id="mode">
55
+ <option value="Character Swap">Character Swap</option>
56
+ <option value="Pose Retarget">Pose Retarget</option>
57
+ </select>
58
 
59
+ <label>کیفیت</label>
60
+ <select id="res">
61
+ <option value="Medium Res">Medium Res</option>
62
+ <option value="High Res">High Res</option>
63
+ </select>
 
 
 
 
 
 
 
64
 
65
+ <button id="runBtn">شروع پردازش (Wan Animate)</button>
 
 
 
 
 
66
 
67
+ <div id="statusBar">
68
+ <div id="statusDot"></div>
69
+ <div id="statusText">آماده شروع</div>
70
+ <div id="statusTimer"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
 
73
+ <div id="log"></div>
74
+ <video id="result" controls style="display:none;"></video>
75
+
76
+ <script>
77
+ const BASE = "https://opera8-wan2-2-animate-zerogpu.hf.space";
78
+ const logEl = document.getElementById('log');
79
+ const resultEl = document.getElementById('result');
80
+ const runBtn = document.getElementById('runBtn');
81
+ const statusDot = document.getElementById('statusDot');
82
+ const statusText = document.getElementById('statusText');
83
+ const statusTimer = document.getElementById('statusTimer');
84
+
85
+ function log(msg) {
86
+ logEl.textContent += msg + "\n";
87
+ logEl.scrollTop = logEl.scrollHeight;
88
+ }
89
+
90
+ // --- تایمر و نوار وضعیت ---
91
+ let timerStart = null;
92
+ let timerInterval = null;
93
+
94
+ function fmtElapsed(ms) {
95
+ const s = Math.floor(ms / 1000);
96
+ const mm = String(Math.floor(s / 60)).padStart(2, "0");
97
+ const ss = String(s % 60).padStart(2, "0");
98
+ return `${mm}:${ss}`;
99
+ }
100
+
101
+ function startTimer() {
102
+ timerStart = Date.now();
103
+ clearInterval(timerInterval);
104
+ timerInterval = setInterval(() => {
105
+ statusTimer.textContent = fmtElapsed(Date.now() - timerStart);
106
+ }, 500);
107
+ }
108
+
109
+ function stopTimer() {
110
+ clearInterval(timerInterval);
111
+ }
112
+
113
+ // state: 'idle' | 'working' | 'done' | 'error'
114
+ function setStatus(state, text) {
115
+ statusDot.className = state === 'idle' ? '' : state;
116
+ statusText.textContent = text;
117
+ if (state === 'working' && !timerInterval) startTimer();
118
+ if (state === 'done' || state === 'error') stopTimer();
119
+ }
120
+
121
+ // تلاش برای استخراج پیام فارسی/انسانی از یک رویداد generating
122
+ function extractFriendlyText(dataLine) {
123
+ try {
124
+ const arr = JSON.parse(dataLine);
125
+ if (Array.isArray(arr)) {
126
+ for (const item of arr) {
127
+ if (item && typeof item === 'object' && typeof item.value === 'string') {
128
+ return item.value.replace(/\*\*/g, "").replace(/[⏳🔄✅⚠️]/g, "").trim();
129
+ }
130
+ }
131
+ }
132
+ } catch (e) { /* داده JSON نبود، مشکلی نیست */ }
133
+ return null;
134
+ }
135
+
136
+ function authHeaders() {
137
+ const token = document.getElementById('hfToken').value.trim();
138
+ return token ? { "Authorization": "Bearer " + token } : {};
139
+ }
140
+
141
+ // آپلود یک فایل به Gradio و برگرداندن مسیر سرور
142
+ async function uploadFile(file) {
143
+ const form = new FormData();
144
+ form.append("files", file, file.name);
145
+ const res = await fetch(`${BASE}/gradio_api/upload`, {
146
+ method: "POST",
147
+ headers: authHeaders(),
148
+ body: form
149
+ });
150
+ if (!res.ok) throw new Error("Upload failed: " + res.status);
151
+ const paths = await res.json(); // آرایه‌ای از مسیرها
152
+ return {
153
+ path: paths[0],
154
+ meta: { "_type": "gradio.FileData" },
155
+ orig_name: file.name
156
+ };
157
+ }
158
+
159
+ // پیدا کردن نام دقیق endpoint از روی /gradio_api/info (در صورت نیاز)
160
+ async function getEndpointName() {
161
+ const res = await fetch(`${BASE}/gradio_api/info`, { headers: authHeaders() });
162
+ const info = await res.json();
163
+ // معمولا نام تابع پایتون همان نام endpoint است، اینجا "animate_scene" را امتحان می‌کنیم
164
+ // اگر پیدا نشد، اولین named_endpoint موجود را برمی‌گردانیم
165
+ const named = info.named_endpoints || {};
166
+ if (named["/animate_scene"]) return "animate_scene";
167
+ const keys = Object.keys(named);
168
+ log("توجه: endpoint دقیق پیدا نشد، اولین مورد موجود انتخاب شد: " + keys[0]);
169
+ return keys[0]?.replace(/^\//, "");
170
+ }
171
+
172
+ // فراخوانی endpoint و دریافت event_id
173
+ async function callEndpoint(endpoint, data) {
174
+ const res = await fetch(`${BASE}/gradio_api/call/${endpoint}`, {
175
+ method: "POST",
176
+ headers: { "Content-Type": "application/json", ...authHeaders() },
177
+ body: JSON.stringify({ data })
178
+ });
179
+ if (!res.ok) throw new Error("Call failed: " + res.status + " " + await res.text());
180
+ const j = await res.json();
181
+ return j.event_id;
182
+ }
183
+
184
+ // یک بار تلاش برای خواندن استریم SSE. اگر استریم بدون 'complete' قطع شود، null برمی‌گرداند (یعنی باید دوباره وصل شد).
185
+ // اگر خود سرور خطای واقعی برگرداند (event: error)، یک خطای fatal پرتاب می‌شود که دیگر نباید retry شود.
186
+ async function attemptStream(url) {
187
+ const res = await fetch(url, { headers: authHeaders() });
188
+ if (!res.ok) throw new Error("HTTP " + res.status);
189
+
190
+ const reader = res.body.getReader();
191
+ const decoder = new TextDecoder();
192
+ let buffer = "";
193
+
194
+ while (true) {
195
+ let chunk;
196
+ try {
197
+ chunk = await reader.read();
198
+ } catch (e) {
199
+ // قطعی شبکه حین خواندن - قابل تلاش مجدد
200
+ throw new Error("network-read-failed");
201
+ }
202
+ const { value, done } = chunk;
203
+ if (done) return null; // اتصال بدون رسیدن به complete بسته شد -> باید reconnect شود
204
+
205
+ buffer += decoder.decode(value, { stream: true });
206
+ const parts = buffer.split("\n\n");
207
+ buffer = parts.pop();
208
+
209
+ for (const part of parts) {
210
+ const lines = part.split("\n");
211
+ let eventType = "message";
212
+ let dataLine = "";
213
+ for (const line of lines) {
214
+ if (line.startsWith("event:")) eventType = line.slice(6).trim();
215
+ if (line.startsWith("data:")) dataLine += line.slice(5).trim();
216
+ }
217
+ if (!dataLine) continue;
218
+
219
+ log(`[${eventType}] ${dataLine.slice(0, 300)}`);
220
+
221
+ if (eventType === "generating" || eventType === "message") {
222
+ const friendly = extractFriendlyText(dataLine);
223
+ if (friendly) setStatus('working', friendly);
224
+ }
225
+
226
+ if (eventType === "complete") {
227
+ return JSON.parse(dataLine);
228
+ }
229
+ if (eventType === "error") {
230
+ const err = new Error("پاسخ خطا از سرور: " + dataLine.slice(0, 300));
231
+ err.fatal = true;
232
+ throw err;
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ // خواندن نتیجه با قابلیت اتصال مجدد خودکار در صورت قطعی شبکه/پراکسی
239
+ async function pollResult(endpoint, eventId) {
240
+ const url = `${BASE}/gradio_api/call/${endpoint}/${eventId}`;
241
+ const maxRetries = 6;
242
+
243
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
244
+ try {
245
+ const result = await attemptStream(url);
246
+ if (result) return result; // به 'complete' رسیدیم
247
+ // استریم بدون خطای صریح ولی بدون complete بسته شد -> تلاش مجدد
248
+ setStatus('working', `اتصال قطع شد، تلاش مجدد برای اتصال (${attempt}/${maxRetries})...`);
249
+ } catch (err) {
250
+ if (err.fatal) {
251
+ setStatus('error', err.message);
252
+ throw err;
253
+ }
254
+ setStatus('working', `خطای شبکه (${err.message})، تلاش مجدد ${attempt}/${maxRetries}...`);
255
+ }
256
+ await new Promise(r => setTimeout(r, 1500 * attempt));
257
+ }
258
+
259
+ const finalErr = new Error("اتصال بعد از چند بار تلاش برقرار نشد. لطفا اتصال اینترنت را بررسی کن و دوباره امتحان کن.");
260
+ setStatus('error', finalErr.message);
261
+ throw finalErr;
262
+ }
263
+
264
+ runBtn.addEventListener('click', async () => {
265
+ const videoInput = document.getElementById('videoFile').files[0];
266
+ const imageInput = document.getElementById('imageFile').files[0];
267
+ if (!videoInput || !imageInput) {
268
+ alert("لطفا هم ویدیو و هم تصویر مرجع را انتخاب کنید.");
269
+ return;
270
+ }
271
+
272
+ runBtn.disabled = true;
273
+ resultEl.style.display = "none";
274
+ logEl.textContent = "";
275
+ statusTimer.textContent = "";
276
+ setStatus('working', 'در حال آپلود ویدیو...');
277
+
278
+ try {
279
+ log("در حال آپلود ویدیو...");
280
+ const videoFileData = await uploadFile(videoInput);
281
+
282
+ setStatus('working', 'در حال آپلود تصویر مرجع...');
283
+ log("در حال آپلود تصویر مرجع...");
284
+ const imageFileData = await uploadFile(imageInput);
285
+
286
+ setStatus('working', 'در حال یافتن endpoint...');
287
+ log("در حال یافتن endpoint...");
288
+ const endpoint = await getEndpointName();
289
+ log("Endpoint: " + endpoint);
290
+
291
+ // ترتیب پارامترها باید دقیقا مطابق ورودی‌های تابع در سمت سرور باشد:
292
+ // [input_video, edited_frame, replace_character_string, resolution_choice, prompt_state, tracking_id_in]
293
+ const data = [
294
+ videoFileData,
295
+ imageFileData,
296
+ document.getElementById('mode').value,
297
+ document.getElementById('res').value,
298
+ { "selections": {}, "active_frame": 0, "cur_label_val": 1.0 }, // prompt_state پیش‌فرض
299
+ null // tracking_id_in (فایل وضعیت resume - اگر نداری null بگذار)
300
+ ];
301
+
302
+ setStatus('working', 'در حال فراخوانی مدل (ممکن است چند دقیقه طول بکشد)...');
303
+ log("در حال فراخوانی مدل (ممکن است طول بکشد)...");
304
+ const eventId = await callEndpoint(endpoint, data);
305
+ log("Event ID: " + eventId);
306
+
307
+ const result = await pollResult(endpoint, eventId);
308
+ log("نتیجه نهایی دریافت شد.");
309
+
310
+ // خروجی اول در result.data معمولا فایل ویدیوی نهایی است
311
+ const outVideo = result.data?.[0];
312
+ if (outVideo && outVideo.url) {
313
+ resultEl.src = outVideo.url;
314
+ resultEl.style.display = "block";
315
+ setStatus('done', `تمام شد ✅ (زمان کل: ${fmtElapsed(Date.now() - timerStart)})`);
316
+ } else {
317
+ setStatus('error', 'پردازش تمام شد اما ویدیوی خروجی در پاسخ نبود — ممکن است نیاز به فایل resume باشد، لاگ زیر را ببین.');
318
+ log("خروجی ویدیو در پاسخ یافت نشد؛ کل پاسخ را در بالا بررسی کنید. ممکن است پردازش نیاز به resume با فایل وضعیت (state file) داشته باشد.");
319
+ }
320
+
321
+ } catch (err) {
322
+ setStatus('error', 'خطا: ' + err.message);
323
+ log("خطا: " + err.message);
324
+ console.error(err);
325
+ } finally {
326
+ runBtn.disabled = false;
327
+ }
328
+ });
329
  </script>
330
 
331
  </body>