prithivMLmods commited on
Commit
8a4b4ee
·
verified ·
1 Parent(s): 5533bd6

update app

Browse files
Files changed (1) hide show
  1. wan2-2_i2v_app.py +44 -1038
wan2-2_i2v_app.py CHANGED
@@ -53,27 +53,26 @@ print("Using device:", device)
53
 
54
  app = Server()
55
 
56
- BASE_DIR = Path(__file__).resolve().parent
57
- OUTPUT_DIR = BASE_DIR / "outputs"
58
- FRAMES_DIR = BASE_DIR / "outputs" / "frames"
59
  EXAMPLES_DIR = BASE_DIR / "example-file"
60
 
61
  OUTPUT_DIR.mkdir(exist_ok=True)
62
  FRAMES_DIR.mkdir(exist_ok=True)
63
 
64
- MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
65
- MAX_DIM = 832
66
- MIN_DIM = 480
67
- SQUARE_DIM = 640
68
- MULTIPLE_OF = 16
69
- MAX_SEED = np.iinfo(np.int32).max
70
- FIXED_FPS = 16
71
- MIN_FRAMES_MODEL = 8
72
- MAX_FRAMES_MODEL = 80
73
- MIN_DURATION = round(MIN_FRAMES_MODEL / FIXED_FPS, 1)
74
- MAX_DURATION = round(MAX_FRAMES_MODEL / FIXED_FPS, 1)
75
 
76
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
77
  DEVICE_LABEL = (
78
  torch.cuda.get_device_name(torch.cuda.current_device()).lower()
79
  if torch.cuda.is_available() else str(device).lower()
@@ -159,6 +158,7 @@ DEFAULT_NEGATIVE_PROMPT = (
159
  "形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
160
  )
161
 
 
162
  def resize_image(image: Image.Image) -> Image.Image:
163
  w, h = image.size
164
  if w == h:
@@ -194,20 +194,15 @@ def get_num_frames(duration_seconds: float) -> int:
194
 
195
 
196
  def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[Image.Image]:
197
- """
198
- Extract one frame per whole second from the generated video.
199
- E.g. 3.5 s → frames at ~0.5 s, ~1.5 s, ~2.5 s (midpoints of each second bucket).
200
- Returns PIL images.
201
- """
202
  cap = cv2.VideoCapture(video_path)
203
  fps = cap.get(cv2.CAP_PROP_FPS) or FIXED_FPS
204
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
205
  total_secs = total_frames / fps
206
 
207
- n_buckets = max(1, int(duration_seconds))
208
  frames_out = []
209
  for i in range(n_buckets):
210
- t = i + 0.5 # midpoint of each second bucket
211
  t = min(t, total_secs - 0.05)
212
  frame_idx = int(t * fps)
213
  cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
@@ -220,11 +215,6 @@ def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[
220
 
221
 
222
  def upscale_frames(pil_frames: List[Image.Image], run_id: str):
223
- """
224
- Upscale each frame with RealESRGAN x2.
225
- Returns list of dicts:
226
- {orig_url, upscaled_url, orig_file, upscaled_file, label}
227
- """
228
  results = []
229
  for i, frame in enumerate(pil_frames):
230
  label = f"Frame {i+1} (~{i+1}s)"
@@ -287,12 +277,11 @@ def infer(
287
  generator=torch.Generator(device="cuda").manual_seed(current_seed),
288
  ).frames[0]
289
 
290
- run_id = uuid.uuid4().hex
291
  vid_name = f"video_{run_id}.mp4"
292
  vid_path = OUTPUT_DIR / vid_name
293
  export_to_video(frames, str(vid_path), fps=FIXED_FPS)
294
 
295
- # Extract + upscale frames
296
  pil_frames = extract_frames_from_video(str(vid_path), duration_seconds)
297
  frame_results = upscale_frames(pil_frames, run_id)
298
 
@@ -319,14 +308,13 @@ def get_example_items():
319
  },
320
  ]
321
 
322
-
323
  @app.get("/example-file/{filename}")
324
  async def example_file(filename: str):
325
  path = EXAMPLES_DIR / filename
326
  if not path.exists():
327
  return JSONResponse({"error": f"Not found: {filename}"}, status_code=404)
328
  suffix = Path(filename).suffix.lower()
329
- mt = {".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".webp":"image/webp"}.get(suffix,"image/jpeg")
330
  return FileResponse(path, media_type=mt)
331
 
332
 
@@ -356,15 +344,15 @@ async def download_frame_file(filename: str):
356
 
357
  @app.post("/api/generate")
358
  async def generate_video_endpoint(
359
- prompt: str = Form(...),
360
- negative_prompt: str = Form(DEFAULT_NEGATIVE_PROMPT),
361
- seed: str = Form("42"),
362
- randomize_seed: str = Form("true"),
363
- duration: str = Form("3.5"),
364
- steps: str = Form("4"),
365
- guidance_scale: str = Form("1.0"),
366
- guidance_scale_2: str = Form("1.0"),
367
- image: Optional[UploadFile] = File(None),
368
  ):
369
  temp_path = None
370
  try:
@@ -406,1006 +394,24 @@ async def generate_video_endpoint(
406
  if temp_path and os.path.exists(temp_path):
407
  os.remove(temp_path)
408
 
409
-
410
  @app.get("/", response_class=HTMLResponse)
411
  async def homepage(request: Request):
412
- examples = get_example_items()
413
- examples_json = json.dumps(examples)
414
- min_dur = MIN_DURATION
415
- max_dur = MAX_DURATION
416
- default_prompt = DEFAULT_PROMPT
417
- default_neg = DEFAULT_NEGATIVE_PROMPT
418
-
419
- return f"""<!DOCTYPE html>
420
- <html lang="en">
421
- <head>
422
- <meta charset="UTF-8" />
423
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
424
- <title>Wan2.2-I2V-Fast</title>
425
- <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
426
- <style>
427
- :root {{
428
- --ub-aubergine: #2C001E;
429
- --ub-aubergine-dark: #1f0015;
430
- --ub-orange: #E95420;
431
- --ub-orange-hover: #c4461a;
432
- --ub-panel: #3D3D3D;
433
- --ub-border: rgba(255,255,255,0.1);
434
- --ub-text: #FFFFFF;
435
- --ub-muted: #b0b0b0;
436
- --ub-input: #2b2b2b;
437
- --panel-radius: 8px;
438
- }}
439
- * {{ box-sizing:border-box; font-family:'Ubuntu',sans-serif; margin:0; padding:0; }}
440
- body {{
441
- background: var(--ub-aubergine);
442
- color: var(--ub-text);
443
- min-height: 100vh;
444
- display: flex;
445
- flex-direction: column;
446
- }}
447
-
448
- /* ── TOPBAR ── */
449
- .topbar {{
450
- background: var(--ub-aubergine-dark);
451
- padding: 0 24px;
452
- border-bottom: 1px solid var(--ub-border);
453
- display: flex;
454
- align-items: center;
455
- justify-content: space-between;
456
- height: 52px;
457
- flex-shrink: 0;
458
- }}
459
- .topbar-title {{
460
- font-weight: 700;
461
- letter-spacing: .5px;
462
- color: var(--ub-orange);
463
- font-size: 15px;
464
- }}
465
- .topbar-examples-btn {{
466
- display: flex;
467
- align-items: center;
468
- gap: 6px;
469
- background: rgba(233,84,32,.12);
470
- border: 1px solid rgba(233,84,32,.35);
471
- color: var(--ub-orange);
472
- padding: 6px 14px;
473
- border-radius: 20px;
474
- font-size: 12px;
475
- font-weight: 600;
476
- cursor: pointer;
477
- transition: background .2s, border-color .2s, transform .15s;
478
- font-family: 'Ubuntu', sans-serif;
479
- letter-spacing: .3px;
480
- white-space: nowrap;
481
- }}
482
- .topbar-examples-btn:hover {{
483
- background: rgba(233,84,32,.22);
484
- border-color: var(--ub-orange);
485
- transform: translateY(-1px);
486
- }}
487
- .btn-thumbs {{ display:flex; gap:3px; margin-right:2px; }}
488
- .btn-thumb {{
489
- width:20px; height:20px; border-radius:3px;
490
- object-fit:cover; border:1px solid rgba(233,84,32,.4); display:block;
491
- }}
492
- .topbar-right {{ display:flex; align-items:center; gap:10px; }}
493
- .topbar-github-btn {{
494
- display: flex;
495
- align-items: center;
496
- gap: 6px;
497
- background: rgba(255,255,255,.06);
498
- border: 1px solid rgba(255,255,255,.18);
499
- color: #e0e0e0;
500
- padding: 6px 13px;
501
- border-radius: 20px;
502
- font-size: 12px;
503
- font-weight: 600;
504
- cursor: pointer;
505
- text-decoration: none;
506
- transition: background .2s, border-color .2s, color .2s, transform .15s;
507
- font-family: 'Ubuntu', sans-serif;
508
- letter-spacing: .3px;
509
- white-space: nowrap;
510
- }}
511
- .topbar-github-btn:hover {{
512
- background: rgba(255,255,255,.13);
513
- border-color: rgba(255,255,255,.4);
514
- color: #ffffff;
515
- transform: translateY(-1px);
516
- }}
517
- .topbar-github-btn svg {{ flex-shrink:0; }}
518
-
519
- /* ── CONTAINER ── */
520
- .container {{
521
- max-width: 1300px;
522
- margin: 0 auto;
523
- padding: 30px 20px;
524
- flex: 1;
525
- width: 100%;
526
- }}
527
- .header-text {{ text-align:center; margin-bottom:30px; }}
528
- .header-text h1 {{ font-size:2.2rem; margin-bottom:10px; }}
529
- .header-text p {{ color:var(--ub-muted); font-size:14px; line-height:1.6; }}
530
- .badge {{
531
- display:inline-block;
532
- background:rgba(233,84,32,.15);
533
- border:1px solid rgba(233,84,32,.4);
534
- color:var(--ub-orange);
535
- font-size:12px; padding:3px 10px;
536
- border-radius:20px; margin-top:8px; font-weight:500;
537
- }}
538
-
539
- /* ── MAIN LAYOUT ── */
540
- .layout {{
541
- display: grid;
542
- grid-template-columns: 440px 1fr;
543
- gap: 24px;
544
- align-items: stretch;
545
- height: 720px;
546
- }}
547
- .panel {{
548
- background: var(--ub-panel);
549
- border-radius: var(--panel-radius);
550
- box-shadow: 0 8px 24px rgba(0,0,0,.25);
551
- display: flex;
552
- flex-direction: column;
553
- overflow: hidden;
554
- height: 100%;
555
- }}
556
- .panel-header {{
557
- padding: 16px 20px;
558
- background: rgba(0,0,0,.2);
559
- border-bottom: 1px solid var(--ub-border);
560
- font-weight: 500;
561
- font-size: 1.05rem;
562
- flex-shrink: 0;
563
- display: flex;
564
- justify-content: space-between;
565
- align-items: center;
566
- }}
567
- .panel-body-scroll {{
568
- flex: 1;
569
- padding: 20px;
570
- overflow-y: auto;
571
- display: flex;
572
- flex-direction: column;
573
- }}
574
-
575
- /* ── FORMS ── */
576
- .form-group {{ margin-bottom:18px; flex-shrink:0; }}
577
- .label {{
578
- display:block; font-weight:500; font-size:13px;
579
- color:var(--ub-muted); margin-bottom:7px;
580
- text-transform:uppercase; letter-spacing:.5px;
581
- }}
582
- .textarea, .input {{
583
- width:100%;
584
- background:var(--ub-input);
585
- border:1px solid var(--ub-border);
586
- color:var(--ub-text);
587
- padding:11px 12px; border-radius:4px;
588
- outline:none; font-size:14px;
589
- font-family:'Ubuntu',sans-serif;
590
- transition:border-color .2s;
591
- }}
592
- .textarea:focus, .input:focus {{ border-color:var(--ub-orange); }}
593
- .textarea {{ min-height:80px; resize:vertical; line-height:1.5; }}
594
-
595
- /* ── UPLOAD ZONE ── */
596
- .upload-zone {{
597
- background:var(--ub-input);
598
- border:1.5px dashed rgba(255,255,255,.2);
599
- border-radius:4px;
600
- cursor:pointer;
601
- transition:background .2s, border-color .2s;
602
- position:relative; overflow:hidden;
603
- aspect-ratio:16/9;
604
- display:flex; align-items:center; justify-content:center;
605
- }}
606
- .upload-zone:hover, .upload-zone.dragover {{
607
- border-color:var(--ub-orange);
608
- background:rgba(233,84,32,.05);
609
- }}
610
- .upload-zone input[type="file"] {{ display:none; }}
611
- .upload-placeholder {{
612
- display:flex; flex-direction:column;
613
- align-items:center; gap:10px;
614
- color:var(--ub-muted); pointer-events:none;
615
- }}
616
- .upload-placeholder svg {{ opacity:.5; }}
617
- .upload-placeholder span {{ font-size:13px; }}
618
- .preview-img {{
619
- position:absolute; inset:0;
620
- width:100%; height:100%;
621
- object-fit:contain; display:none; background:#111;
622
- }}
623
- .remove-img-btn {{
624
- position:absolute; top:8px; right:8px;
625
- background:rgba(0,0,0,.7); border:none; color:white;
626
- border-radius:50%; width:26px; height:26px;
627
- display:none; align-items:center; justify-content:center;
628
- cursor:pointer; font-size:14px; z-index:5;
629
- transition:background .2s;
630
- }}
631
- .remove-img-btn:hover {{ background:var(--ub-orange); }}
632
-
633
- /* ── SLIDER INPUT ── */
634
- .slider-wrap {{ display:flex; align-items:center; gap:12px; }}
635
- .slider-wrap input[type="range"] {{
636
- flex:1; -webkit-appearance:none;
637
- height:4px; background:var(--ub-input);
638
- border-radius:2px; outline:none; cursor:pointer;
639
- border:1px solid var(--ub-border);
640
- }}
641
- .slider-wrap input[type="range"]::-webkit-slider-thumb {{
642
- -webkit-appearance:none;
643
- width:16px; height:16px; border-radius:50%;
644
- background:var(--ub-orange); cursor:pointer;
645
- }}
646
- .slider-val {{
647
- min-width:40px; text-align:right;
648
- font-size:13px; color:var(--ub-text); font-weight:500;
649
- }}
650
-
651
- /* ── ADVANCED ACCORDION ── */
652
- .advanced-toggle {{
653
- width:100%; background:none;
654
- border:none; border-top:1px solid var(--ub-border);
655
- color:var(--ub-orange); text-align:left;
656
- padding:12px 0; font-weight:500; font-size:13px;
657
- cursor:pointer; display:flex;
658
- justify-content:space-between; align-items:center;
659
- flex-shrink:0; margin-top:4px;
660
- }}
661
- .adv-icon {{ font-weight:bold; font-size:18px; line-height:1; }}
662
- .advanced-body {{ display:none; padding-top:12px; flex-shrink:0; }}
663
- .advanced-body.open {{ display:block; }}
664
- .grid-2 {{ display:grid; grid-template-columns:1fr 1fr; gap:14px; }}
665
-
666
- /* ── STATUS LOG ── */
667
- .status-container {{
668
- margin-top:16px; margin-bottom:16px;
669
- border:1px solid var(--ub-border);
670
- border-radius:4px; background:#200014;
671
- display:flex; flex-direction:column;
672
- flex:1; min-height:80px; max-height:150px;
673
- }}
674
- .status-header {{
675
- padding:7px 12px; font-size:10px; font-weight:700;
676
- color:var(--ub-muted); background:rgba(0,0,0,.4);
677
- border-bottom:1px solid var(--ub-border);
678
- text-transform:uppercase; letter-spacing:.6px; flex-shrink:0;
679
- }}
680
- .status-log {{
681
- padding:8px 10px;
682
- font-family:'Courier New',Courier,monospace;
683
- font-size:11.5px; color:#eee; overflow-y:auto;
684
- flex:1; display:flex; flex-direction:column; gap:3px;
685
- }}
686
- .log-time {{ color:#666; margin-right:7px; }}
687
- .log-info {{ color:#5bc0eb; }}
688
- .log-success {{ color:#9bc53d; }}
689
- .log-error {{ color:#ff5e5b; }}
690
- .log-warn {{ color:#f4c842; }}
691
-
692
- /* ── BUTTONS ── */
693
- .btn {{
694
- width:100%; padding:13px; border:none; border-radius:4px;
695
- font-size:15px; font-weight:700; cursor:pointer;
696
- transition:opacity .2s, background .2s; flex-shrink:0;
697
- letter-spacing:.3px; font-family:'Ubuntu',sans-serif;
698
- }}
699
- .btn-primary {{
700
- background:var(--ub-orange); color:white;
701
- box-shadow:0 4px 14px rgba(233,84,32,.3);
702
- }}
703
- .btn-primary:hover {{ background:var(--ub-orange-hover); }}
704
- .btn:disabled {{ opacity:.55; cursor:not-allowed; }}
705
-
706
- .action-icon {{
707
- display:none; background:none; border:none;
708
- color:var(--ub-muted); cursor:pointer; padding:4px;
709
- transition:color .2s;
710
- }}
711
- .action-icon:hover {{ color:var(--ub-orange); }}
712
-
713
- /* ── OUTPUT PANEL ── */
714
- .panel-body-output {{
715
- flex:1; display:flex; flex-direction:column;
716
- padding:0; position:relative;
717
- }}
718
- .output-stage {{
719
- position:absolute; top:0; left:0; right:0; bottom:0;
720
- background:#0d0008; overflow:hidden;
721
- display:flex; align-items:center; justify-content:center;
722
- }}
723
- .output-empty {{ color:var(--ub-muted); text-align:center; z-index:1; }}
724
- .output-empty svg {{ opacity:.3; margin-bottom:12px; }}
725
- .output-empty div {{ font-size:14px; }}
726
- .output-video {{
727
- position:absolute; top:0; left:0;
728
- width:100%; height:100%;
729
- object-fit:contain; display:none; background:#000;
730
- }}
731
-
732
- /* ── LOADER ── */
733
- .loader {{
734
- position:absolute; inset:0;
735
- background:rgba(15,0,10,.78);
736
- backdrop-filter:blur(6px);
737
- display:none; flex-direction:column;
738
- align-items:center; justify-content:center; z-index:20;
739
- }}
740
- .spinner-single {{
741
- width:52px; height:52px;
742
- border:3px solid rgba(255,255,255,.08);
743
- border-top-color:var(--ub-orange);
744
- border-radius:50%;
745
- animation:spin 1s cubic-bezier(.4,0,.2,1) infinite;
746
- margin-bottom:18px;
747
- }}
748
- .loader-text {{
749
- font-weight:500; font-size:14px; color:#fff;
750
- letter-spacing:1px;
751
- animation:pulse 1.5s ease-in-out infinite;
752
- }}
753
- .loader-sub {{ font-size:12px; color:var(--ub-muted); margin-top:6px; }}
754
- @keyframes pulse {{ 0%,100%{{opacity:1}} 50%{{opacity:.45}} }}
755
- @keyframes spin {{ to{{transform:rotate(360deg)}} }}
756
-
757
- .seed-badge {{
758
- display:none; position:absolute; bottom:12px; right:12px;
759
- background:rgba(0,0,0,.65); color:#ccc;
760
- padding:5px 10px; border-radius:20px;
761
- font-size:12px; backdrop-filter:blur(4px); z-index:5;
762
- }}
763
- .checkbox-label {{
764
- display:flex; align-items:center; gap:8px;
765
- font-size:14px; color:var(--ub-text); cursor:pointer;
766
- }}
767
- .checkbox-label input {{
768
- cursor:pointer; accent-color:var(--ub-orange); width:15px; height:15px;
769
- }}
770
-
771
- /* ── FRAME UPSCALE SECTION ── */
772
- .frames-section {{
773
- margin-top:36px;
774
- }}
775
- .frames-section-header {{
776
- display:flex; align-items:center; gap:12px;
777
- border-bottom:1px solid var(--ub-border);
778
- padding-bottom:12px; margin-bottom:20px;
779
- }}
780
- .frames-section-header h3 {{
781
- font-size:1.1rem; color:var(--ub-text);
782
- }}
783
- .frames-badge {{
784
- background:rgba(155,197,61,.15);
785
- border:1px solid rgba(155,197,61,.35);
786
- color:#9bc53d;
787
- font-size:11px; font-weight:600;
788
- padding:3px 9px; border-radius:12px;
789
- }}
790
- .frames-grid {{
791
- display:grid;
792
- grid-template-columns:repeat(auto-fill,minmax(320px,1fr));
793
- gap:22px;
794
- }}
795
-
796
- /* ── COMPARISON CARD ── */
797
- .cmp-card {{
798
- background:var(--ub-panel);
799
- border-radius:var(--panel-radius);
800
- overflow:hidden;
801
- border:1px solid var(--ub-border);
802
- box-shadow:0 4px 16px rgba(0,0,0,.25);
803
- }}
804
- .cmp-card-header {{
805
- display:flex; align-items:center;
806
- justify-content:space-between;
807
- padding:10px 14px;
808
- background:rgba(0,0,0,.25);
809
- border-bottom:1px solid var(--ub-border);
810
- font-size:13px; font-weight:500;
811
- }}
812
- .cmp-label {{ color:var(--ub-text); }}
813
- .cmp-dl-btn {{
814
- display:flex; align-items:center; gap:5px;
815
- background:rgba(233,84,32,.15);
816
- border:1px solid rgba(233,84,32,.35);
817
- color:var(--ub-orange);
818
- padding:4px 10px; border-radius:12px;
819
- font-size:11px; font-weight:600;
820
- cursor:pointer; transition:background .2s;
821
- font-family:'Ubuntu',sans-serif;
822
- }}
823
- .cmp-dl-btn:hover {{ background:rgba(233,84,32,.3); }}
824
-
825
- /* ── SLIDER COMPARE ── */
826
- .cmp-stage {{
827
- position:relative;
828
- width:100%;
829
- aspect-ratio:16/9;
830
- overflow:hidden;
831
- cursor:ew-resize;
832
- user-select:none;
833
- background:#111;
834
- }}
835
- .cmp-img {{
836
- position:absolute; top:0; left:0;
837
- width:100%; height:100%;
838
- object-fit:contain;
839
- }}
840
- .cmp-img-before {{ z-index:1; }}
841
- .cmp-img-after {{
842
- z-index:2;
843
- clip-path:inset(0 50% 0 0); /* initially right half hidden */
844
- }}
845
- /* the draggable divider */
846
- .cmp-divider {{
847
- position:absolute; top:0; bottom:0;
848
- left:50%;
849
- width:2px;
850
- background:var(--ub-orange);
851
- z-index:10;
852
- pointer-events:none;
853
- }}
854
- .cmp-handle {{
855
- position:absolute; top:50%;
856
- left:50%;
857
- transform:translate(-50%,-50%);
858
- width:32px; height:32px;
859
- border-radius:50%;
860
- background:var(--ub-orange);
861
- z-index:11;
862
- display:flex; align-items:center; justify-content:center;
863
- pointer-events:none;
864
- box-shadow:0 2px 8px rgba(0,0,0,.5);
865
- }}
866
- .cmp-handle svg {{ flex-shrink:0; }}
867
- /* labels */
868
- .cmp-pill {{
869
- position:absolute; bottom:10px;
870
- background:rgba(0,0,0,.65);
871
- color:#fff; font-size:10px; font-weight:700;
872
- padding:3px 8px; border-radius:10px;
873
- z-index:12; pointer-events:none;
874
- backdrop-filter:blur(3px);
875
- letter-spacing:.4px;
876
- }}
877
- .cmp-pill-left {{ left:10px; }}
878
- .cmp-pill-right {{ right:10px; color:#9bc53d; }}
879
-
880
- /* ── EXAMPLES SECTION ── */
881
- .examples-section {{ margin-top:40px; scroll-margin-top:20px; }}
882
- .examples-section-header {{
883
- display:flex; align-items:center; justify-content:space-between;
884
- border-bottom:1px solid var(--ub-border);
885
- padding-bottom:12px; margin-bottom:20px;
886
- }}
887
- .examples-section-header h3 {{ font-size:1.1rem; color:var(--ub-text); }}
888
- .examples-grid {{
889
- display:grid;
890
- grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
891
- gap:18px;
892
- }}
893
- .ex-card {{
894
- background:var(--ub-panel); border-radius:6px;
895
- overflow:hidden; cursor:pointer;
896
- transition:transform .2s, box-shadow .2s, border-color .2s;
897
- border:1px solid var(--ub-border);
898
- }}
899
- .ex-card:hover {{
900
- transform:translateY(-4px);
901
- box-shadow:0 10px 28px rgba(0,0,0,.4);
902
- border-color:rgba(233,84,32,.5);
903
- }}
904
- .ex-img-wrap {{
905
- width:100%; aspect-ratio:16/9;
906
- background:#1a001a; overflow:hidden; position:relative;
907
- }}
908
- .ex-img-wrap.loading::before {{
909
- content:''; position:absolute; inset:0;
910
- background:linear-gradient(90deg,#2c001e 25%,#3d1030 50%,#2c001e 75%);
911
- background-size:200% 100%;
912
- animation:shimmer 1.5s infinite; z-index:1;
913
- }}
914
- @keyframes shimmer {{
915
- 0%{{background-position:-200% 0}} 100%{{background-position:200% 0}}
916
- }}
917
- .ex-img-wrap img {{
918
- width:100%; height:100%; object-fit:cover; display:block;
919
- transition:transform .3s; position:relative; z-index:2;
920
- }}
921
- .ex-card:hover .ex-img-wrap img {{ transform:scale(1.04); }}
922
- .ex-use-badge {{
923
- position:absolute; top:8px; left:8px; z-index:3;
924
- background:var(--ub-orange); color:white;
925
- font-size:10px; font-weight:700; padding:3px 8px; border-radius:10px;
926
- opacity:0; transition:opacity .2s; pointer-events:none;
927
- }}
928
- .ex-card:hover .ex-use-badge {{ opacity:1; }}
929
- .ex-card p {{
930
- padding:10px 12px 13px; font-size:12px;
931
- color:var(--ub-muted); line-height:1.5;
932
- display:-webkit-box; -webkit-line-clamp:3;
933
- -webkit-box-orient:vertical; overflow:hidden;
934
- }}
935
-
936
- @media(max-width:900px) {{
937
- .layout {{ grid-template-columns:1fr; height:auto; }}
938
- .panel-body-output {{ height:380px; flex:none; }}
939
- .output-stage {{ position:relative; height:100%; }}
940
- .btn-thumbs {{ display:none; }}
941
- .frames-grid {{ grid-template-columns:1fr; }}
942
- }}
943
- </style>
944
- </head>
945
- <body>
946
-
947
- <div class="topbar">
948
- <span class="topbar-title">Wan2.2-I2V-Fast — Lightning LoRA · FP8 · AoT Compiled</span>
949
- <div class="topbar-right">
950
-
951
- <!-- GitHub link -->
952
- <a class="topbar-github-btn"
953
- href="https://github.com/PRITHIVSAKTHIUR/wan2.2-i2v-fast"
954
- target="_blank" rel="noopener noreferrer" title="View source on GitHub">
955
- <svg width="15" height="15" fill="currentColor" viewBox="0 0 24 24">
956
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385
957
- .6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235
958
- -3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695
959
- -.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23
960
- 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605
961
- -2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225
962
- -.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23
963
- .96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23
964
- 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225
965
- 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22
966
- 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57
967
- A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
968
- </svg>
969
- GitHub
970
- </a>
971
-
972
- <!-- Examples scroll button -->
973
- <button class="topbar-examples-btn" id="examplesScrollBtn" title="Browse examples">
974
- <div class="btn-thumbs" id="btnThumbs"></div>
975
- <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"
976
- stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" style="margin-right:4px;">
977
- <rect x="3" y="3" width="7" height="7" rx="1"></rect>
978
- <rect x="14" y="3" width="7" height="7" rx="1"></rect>
979
- <rect x="3" y="14" width="7" height="7" rx="1"></rect>
980
- <rect x="14" y="14" width="7" height="7" rx="1"></rect>
981
- </svg>
982
- Examples
983
- </button>
984
-
985
- </div>
986
- </div>
987
-
988
- <div class="container">
989
- <div class="header-text">
990
- <h1>Wan2.2 Image-to-Video</h1>
991
- <p>Upload an image and describe the animation. Generates video in 4–8 steps with Lightning LoRA.<br>
992
- Frames are automatically extracted &amp; upscaled with RealESRGAN ×2.</p>
993
- <span class="badge">14B · FP8 Quantized · ZeroGPU · RealESRGAN Upscale</span>
994
- </div>
995
-
996
- <div class="layout">
997
- <!-- Left: Settings -->
998
- <div class="panel">
999
- <div class="panel-header">Settings</div>
1000
- <div class="panel-body-scroll">
1001
-
1002
- <div class="form-group">
1003
- <label class="label">Input Image</label>
1004
- <div class="upload-zone" id="dropZone">
1005
- <input type="file" id="fileInput" accept="image/*" />
1006
- <div class="upload-placeholder" id="uploadPlaceholder">
1007
- <svg width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
1008
- <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
1009
- </svg>
1010
- <span>Click or drag &amp; drop an image</span>
1011
- </div>
1012
- <img id="previewImg" class="preview-img" alt="Preview" />
1013
- <button id="removeImgBtn" class="remove-img-btn" title="Remove">×</button>
1014
- </div>
1015
- </div>
1016
-
1017
- <div class="form-group">
1018
- <label class="label">Prompt</label>
1019
- <textarea id="promptInput" class="textarea" placeholder="Describe the animation...">{default_prompt}</textarea>
1020
- </div>
1021
-
1022
- <div class="form-group">
1023
- <label class="label">Duration — <span id="durationVal">3.5</span>s</label>
1024
- <div class="slider-wrap">
1025
- <input type="range" id="duration" min="{min_dur}" max="{max_dur}" step="0.1" value="3.5"
1026
- oninput="document.getElementById('durationVal').textContent=parseFloat(this.value).toFixed(1)">
1027
- <span class="slider-val">{min_dur}–{max_dur}s</span>
1028
- </div>
1029
- </div>
1030
-
1031
- <button class="advanced-toggle" id="advToggle">
1032
- <span>Advanced Settings</span>
1033
- <span class="adv-icon" id="advIcon">+</span>
1034
- </button>
1035
- <div class="advanced-body" id="advBody">
1036
- <div class="form-group">
1037
- <label class="label">Negative Prompt</label>
1038
- <textarea id="negPrompt" class="textarea" style="min-height:60px;font-size:12px;">{default_neg}</textarea>
1039
- </div>
1040
- <div class="grid-2">
1041
- <div class="form-group">
1042
- <label class="label">Steps</label>
1043
- <input type="number" id="steps" class="input" value="4" min="1" max="30">
1044
- </div>
1045
- <div class="form-group">
1046
- <label class="label">Seed</label>
1047
- <input type="number" id="seed" class="input" value="42">
1048
- </div>
1049
- <div class="form-group">
1050
- <label class="label">Guidance (Stage 1)</label>
1051
- <input type="number" id="guidance" class="input" value="1.0" step="0.5" min="0" max="10">
1052
- </div>
1053
- <div class="form-group">
1054
- <label class="label">Guidance (Stage 2)</label>
1055
- <input type="number" id="guidance2" class="input" value="1.0" step="0.5" min="0" max="10">
1056
- </div>
1057
- <div class="form-group" style="grid-column:span 2">
1058
- <label class="checkbox-label">
1059
- <input type="checkbox" id="randomize" checked> Randomize Seed
1060
- </label>
1061
- </div>
1062
- </div>
1063
- </div>
1064
-
1065
- <div class="status-container">
1066
- <div class="status-header">Execution Log</div>
1067
- <div class="status-log" id="statusLog">
1068
- <div><span class="log-time">[{DEVICE_LABEL}]</span><span>System ready...</span></div>
1069
- </div>
1070
- </div>
1071
-
1072
- <button class="btn btn-primary" id="runBtn">Generate Video</button>
1073
- </div>
1074
- </div>
1075
-
1076
- <!-- Right: Output -->
1077
- <div class="panel">
1078
- <div class="panel-header">
1079
- <span>Output</span>
1080
- <button id="downloadBtn" class="action-icon" title="Download Video">
1081
- <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"
1082
- stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1083
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
1084
- <polyline points="7 10 12 15 17 10"></polyline>
1085
- <line x1="12" y1="15" x2="12" y2="3"></line>
1086
- </svg>
1087
- </button>
1088
- </div>
1089
- <div class="panel-body-output">
1090
- <div class="output-stage" id="outputStage">
1091
- <div class="output-empty" id="outputEmpty">
1092
- <svg width="52" height="52" fill="none" stroke="currentColor" stroke-width="1.2" viewBox="0 0 24 24">
1093
- <polygon points="23 7 16 12 23 17 23 7"></polygon>
1094
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
1095
- </svg>
1096
- <div>Video will appear here</div>
1097
- </div>
1098
- <video id="outputVideo" class="output-video" controls autoplay loop></video>
1099
- <div class="seed-badge" id="seedBadge"></div>
1100
- <div class="loader" id="loader">
1101
- <div class="spinner-single"></div>
1102
- <div class="loader-text">Generating video + upscaling frames...</div>
1103
- <div class="loader-sub">This may take a minute</div>
1104
- </div>
1105
- </div>
1106
- </div>
1107
- </div>
1108
- </div>
1109
-
1110
- <!-- Frame Upscale Results -->
1111
- <div class="frames-section" id="framesSection" style="display:none;">
1112
- <div class="frames-section-header">
1113
- <h3>Frame Upscale Results</h3>
1114
- <span class="frames-badge" id="framesBadge">RealESRGAN ×2</span>
1115
- </div>
1116
- <div class="frames-grid" id="framesGrid"></div>
1117
- </div>
1118
-
1119
- <!-- Examples -->
1120
- <div class="examples-section" id="examplesSection">
1121
- <div class="examples-section-header">
1122
- <h3>Examples</h3>
1123
- </div>
1124
- <div class="examples-grid" id="examplesGrid"></div>
1125
- </div>
1126
- </div>
1127
-
1128
- <script>
1129
- const examples = {examples_json};
1130
- let currentFile = null;
1131
- let currentFilename = "";
1132
-
1133
- const dropZone = document.getElementById('dropZone');
1134
- const fileInput = document.getElementById('fileInput');
1135
- const uploadPlaceholder = document.getElementById('uploadPlaceholder');
1136
- const previewImg = document.getElementById('previewImg');
1137
- const removeImgBtn = document.getElementById('removeImgBtn');
1138
- const promptInput = document.getElementById('promptInput');
1139
- const runBtn = document.getElementById('runBtn');
1140
- const downloadBtn = document.getElementById('downloadBtn');
1141
- const statusLog = document.getElementById('statusLog');
1142
- const outputVideo = document.getElementById('outputVideo');
1143
- const outputEmpty = document.getElementById('outputEmpty');
1144
- const loader = document.getElementById('loader');
1145
- const seedBadge = document.getElementById('seedBadge');
1146
- const framesSection = document.getElementById('framesSection');
1147
- const framesGrid = document.getElementById('framesGrid');
1148
- const examplesSection = document.getElementById('examplesSection');
1149
-
1150
- /* ── scroll to examples ── */
1151
- document.getElementById('examplesScrollBtn').onclick = () =>
1152
- examplesSection.scrollIntoView({{behavior:'smooth',block:'start'}});
1153
-
1154
- /* ── topbar thumbnails ── */
1155
- const btnThumbs = document.getElementById('btnThumbs');
1156
- examples.slice(0,3).forEach(ex => {{
1157
- const img = document.createElement('img');
1158
- img.src = ex.url; img.className = 'btn-thumb'; img.alt = '';
1159
- img.onerror = () => {{ img.style.display='none'; }};
1160
- btnThumbs.appendChild(img);
1161
- }});
1162
-
1163
- /* ── log ── */
1164
- function logMsg(msg, cls='') {{
1165
- const d = document.createElement('div');
1166
- const t = new Date().toLocaleTimeString('en-US',{{hour12:false}});
1167
- d.innerHTML = `<span class="log-time">[${{t}}]</span><span class="${{cls}}">${{msg}}</span>`;
1168
- statusLog.appendChild(d);
1169
- statusLog.scrollTop = statusLog.scrollHeight;
1170
- }}
1171
-
1172
- /* ── advanced ── */
1173
- document.getElementById('advToggle').onclick = function() {{
1174
- const b = document.getElementById('advBody');
1175
- b.classList.toggle('open');
1176
- document.getElementById('advIcon').innerText = b.classList.contains('open') ? '−' : '+';
1177
- }};
1178
-
1179
- /* ── image upload ── */
1180
- function setImage(file) {{
1181
- if (!file) return;
1182
- currentFile = file;
1183
- previewImg.src = URL.createObjectURL(file);
1184
- previewImg.style.display = 'block';
1185
- uploadPlaceholder.style.display = 'none';
1186
- removeImgBtn.style.display = 'flex';
1187
- logMsg(`Image loaded: ${{file.name}}`,'log-info');
1188
- }}
1189
- function clearImage() {{
1190
- currentFile = null;
1191
- previewImg.src = '';
1192
- previewImg.style.display = 'none';
1193
- uploadPlaceholder.style.display = 'flex';
1194
- removeImgBtn.style.display = 'none';
1195
- fileInput.value = '';
1196
- }}
1197
- dropZone.onclick = e => {{
1198
- if (e.target===dropZone||e.target===uploadPlaceholder||uploadPlaceholder.contains(e.target))
1199
- fileInput.click();
1200
- }};
1201
- fileInput.onchange = e => {{ if(e.target.files[0]) setImage(e.target.files[0]); fileInput.value=''; }};
1202
- dropZone.ondragover = e => {{ e.preventDefault(); dropZone.classList.add('dragover'); }};
1203
- dropZone.ondragleave = ()=> dropZone.classList.remove('dragover');
1204
- dropZone.ondrop = e => {{
1205
- e.preventDefault(); dropZone.classList.remove('dragover');
1206
- if(e.dataTransfer.files[0]) setImage(e.dataTransfer.files[0]);
1207
- }};
1208
- removeImgBtn.onclick = e => {{ e.stopPropagation(); clearImage(); }};
1209
-
1210
- /* ── download video ── */
1211
- downloadBtn.onclick = () => {{
1212
- if(!currentFilename) return;
1213
- const a=document.createElement('a'); a.href=`/download/${{currentFilename}}`; a.download=currentFilename; a.click();
1214
- }};
1215
-
1216
- /* ── comparison slider logic ── */
1217
- function initCompare(stage) {{
1218
- const after = stage.querySelector('.cmp-img-after');
1219
- const divider = stage.querySelector('.cmp-divider');
1220
- const handle = stage.querySelector('.cmp-handle');
1221
- let dragging = false;
1222
-
1223
- function setPos(x) {{
1224
- const rect = stage.getBoundingClientRect();
1225
- let pct = Math.max(0, Math.min(1, (x - rect.left) / rect.width));
1226
- const pctPx = (pct*100).toFixed(2);
1227
- after.style.clipPath = `inset(0 ${{(100-pct*100).toFixed(2)}}% 0 0)`;
1228
- divider.style.left = pctPx + '%';
1229
- handle.style.left = pctPx + '%';
1230
- }}
1231
-
1232
- stage.addEventListener('mousedown', e=>{{ dragging=true; setPos(e.clientX); }});
1233
- window.addEventListener('mouseup', ()=>{{ dragging=false; }});
1234
- window.addEventListener('mousemove', e=>{{ if(dragging) setPos(e.clientX); }});
1235
-
1236
- stage.addEventListener('touchstart', e=>{{ dragging=true; setPos(e.touches[0].clientX); }},{{passive:true}});
1237
- window.addEventListener('touchend', ()=>{{ dragging=false; }});
1238
- window.addEventListener('touchmove', e=>{{ if(dragging) setPos(e.touches[0].clientX); }},{{passive:true}});
1239
- }}
1240
-
1241
- /* ── build frame comparison cards ── */
1242
- function buildFrameCards(frameResults) {{
1243
- framesGrid.innerHTML = '';
1244
- if(!frameResults || frameResults.length===0) return;
1245
-
1246
- framesSection.style.display = 'block';
1247
- document.getElementById('framesBadge').textContent =
1248
- `RealESRGAN ×2 — ${{frameResults.length}} frame${{frameResults.length>1?'s':''}}`;
1249
-
1250
- frameResults.forEach(fr => {{
1251
- const card = document.createElement('div');
1252
- card.className = 'cmp-card';
1253
-
1254
- // header
1255
- const hdr = document.createElement('div');
1256
- hdr.className = 'cmp-card-header';
1257
- hdr.innerHTML = `
1258
- <span class="cmp-label">${{fr.label}}</span>
1259
- <button class="cmp-dl-btn" onclick="dlFrame('${{fr.upscaled_file}}')">
1260
- <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5"
1261
- stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1262
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
1263
- <polyline points="7 10 12 15 17 10"></polyline>
1264
- <line x1="12" y1="15" x2="12" y2="3"></line>
1265
- </svg>
1266
- Download upscaled
1267
- </button>`;
1268
-
1269
- // stage
1270
- const stage = document.createElement('div');
1271
- stage.className = 'cmp-stage';
1272
- stage.innerHTML = `
1273
- <img class="cmp-img cmp-img-before" src="${{fr.orig_url}}" alt="Original">
1274
- <img class="cmp-img cmp-img-after" src="${{fr.upscaled_url}}" alt="Upscaled">
1275
- <div class="cmp-divider"></div>
1276
- <div class="cmp-handle">
1277
- <svg width="16" height="16" fill="none" stroke="white" stroke-width="2.5"
1278
- stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1279
- <polyline points="15 18 9 12 15 6"></polyline>
1280
- <polyline points="9 18 3 12 9 6" style="transform:translateX(6px)"></polyline>
1281
- </svg>
1282
- </div>
1283
- <span class="cmp-pill cmp-pill-left">ORIGINAL</span>
1284
- <span class="cmp-pill cmp-pill-right">UPSCALED ×2</span>`;
1285
-
1286
- card.appendChild(hdr);
1287
- card.appendChild(stage);
1288
- framesGrid.appendChild(card);
1289
- initCompare(stage);
1290
- }});
1291
-
1292
-
1293
- }}
1294
-
1295
- function dlFrame(filename) {{
1296
- const a=document.createElement('a'); a.href=`/download-frame/${{filename}}`; a.download=filename; a.click();
1297
- }}
1298
-
1299
- /* ── generate ── */
1300
- runBtn.onclick = async () => {{
1301
- const prompt = promptInput.value.trim();
1302
- if(!prompt) {{ logMsg('Prompt is required.','log-error'); return; }}
1303
- if(!currentFile) {{ logMsg('Please upload an input image first.','log-error'); return; }}
1304
-
1305
- logMsg('Initializing generation...','log-info');
1306
-
1307
- const fd = new FormData();
1308
- fd.append('prompt', prompt);
1309
- fd.append('negative_prompt', document.getElementById('negPrompt').value);
1310
- fd.append('seed', document.getElementById('seed').value);
1311
- fd.append('randomize_seed', document.getElementById('randomize').checked);
1312
- fd.append('duration', document.getElementById('duration').value);
1313
- fd.append('steps', document.getElementById('steps').value);
1314
- fd.append('guidance_scale', document.getElementById('guidance').value);
1315
- fd.append('guidance_scale_2', document.getElementById('guidance2').value);
1316
- fd.append('image', currentFile);
1317
-
1318
- loader.style.display = 'flex';
1319
- runBtn.disabled = true;
1320
- downloadBtn.style.display = 'none';
1321
- seedBadge.style.display = 'none';
1322
- outputVideo.style.display = 'none';
1323
- outputEmpty.style.display = 'flex';
1324
- framesSection.style.display = 'none';
1325
-
1326
- logMsg('Sending to Wan 2.2 I2V pipeline (FP8 + Lightning LoRA + RealESRGAN)...','log-info');
1327
-
1328
- try {{
1329
- const res = await fetch('/api/generate', {{method:'POST', body:fd}});
1330
- const data = await res.json();
1331
-
1332
- if(data.success) {{
1333
- logMsg(`Video done! Seed: ${{data.seed}}`, 'log-success');
1334
- logMsg(`Upscaled ${{(data.frame_results||[]).length}} frame(s) with RealESRGAN ×2`, 'log-success');
1335
- currentFilename = data.filename;
1336
-
1337
- outputVideo.src = data.url;
1338
- outputVideo.style.display = 'block';
1339
- outputEmpty.style.display = 'none';
1340
- downloadBtn.style.display = 'block';
1341
- seedBadge.innerText = `Seed: ${{data.seed}}`;
1342
- seedBadge.style.display = 'block';
1343
-
1344
- buildFrameCards(data.frame_results || []);
1345
- }} else {{
1346
- logMsg('Error: ' + data.error, 'log-error');
1347
- }}
1348
- }} catch(e) {{
1349
- logMsg('Network or server error.','log-error');
1350
- }} finally {{
1351
- loader.style.display = 'none';
1352
- runBtn.disabled = false;
1353
- logMsg('Ready for next input.','');
1354
- }}
1355
- }};
1356
-
1357
- /* ── load example ── */
1358
- async function loadExample(url, prompt) {{
1359
- clearImage();
1360
- promptInput.value = prompt;
1361
- logMsg('Loading example image...','log-info');
1362
- try {{
1363
- const res = await fetch(url);
1364
- if(!res.ok) throw new Error(`HTTP ${{res.status}}`);
1365
- const blob = await res.blob();
1366
- const filename = url.split('/').pop();
1367
- setImage(new File([blob], filename, {{type:blob.type||'image/jpeg'}}));
1368
- window.scrollTo({{top:0, behavior:'smooth'}});
1369
- logMsg('Example loaded — ready to generate.','log-success');
1370
- }} catch(e) {{
1371
- logMsg(`Failed to load example: ${{e.message}}`, 'log-error');
1372
- }}
1373
- }}
1374
-
1375
- /* ── build example cards ── */
1376
- const exGrid = document.getElementById('examplesGrid');
1377
- examples.forEach(ex => {{
1378
- const card = document.createElement('div');
1379
- card.className = 'ex-card';
1380
-
1381
- const wrap = document.createElement('div');
1382
- wrap.className = 'ex-img-wrap loading';
1383
-
1384
- const img = document.createElement('img');
1385
- img.alt = 'Example'; img.loading = 'lazy';
1386
- img.onload = () => {{ wrap.classList.remove('loading'); }};
1387
- img.onerror = () => {{
1388
- wrap.classList.remove('loading');
1389
- wrap.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#555;font-size:12px;">No preview</div>`;
1390
- }};
1391
- img.src = ex.url;
1392
-
1393
- const badge = document.createElement('span');
1394
- badge.className = 'ex-use-badge'; badge.textContent = 'Use this';
1395
-
1396
- wrap.appendChild(img); wrap.appendChild(badge);
1397
-
1398
- const p = document.createElement('p');
1399
- p.textContent = ex.prompt;
1400
-
1401
- card.appendChild(wrap); card.appendChild(p);
1402
- card.onclick = () => loadExample(ex.url, ex.prompt);
1403
- exGrid.appendChild(card);
1404
- }});
1405
- </script>
1406
- </body>
1407
- </html>
1408
- """
1409
 
1410
  if __name__ == "__main__":
1411
  app.launch(show_error=True)
 
53
 
54
  app = Server()
55
 
56
+ BASE_DIR = Path(__file__).resolve().parent
57
+ OUTPUT_DIR = BASE_DIR / "outputs"
58
+ FRAMES_DIR = BASE_DIR / "outputs" / "frames"
59
  EXAMPLES_DIR = BASE_DIR / "example-file"
60
 
61
  OUTPUT_DIR.mkdir(exist_ok=True)
62
  FRAMES_DIR.mkdir(exist_ok=True)
63
 
64
+ MODEL_ID = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
65
+ MAX_DIM = 832
66
+ MIN_DIM = 480
67
+ SQUARE_DIM = 640
68
+ MULTIPLE_OF = 16
69
+ MAX_SEED = np.iinfo(np.int32).max
70
+ FIXED_FPS = 16
71
+ MIN_FRAMES_MODEL = 8
72
+ MAX_FRAMES_MODEL = 80
73
+ MIN_DURATION = round(MIN_FRAMES_MODEL / FIXED_FPS, 1)
74
+ MAX_DURATION = round(MAX_FRAMES_MODEL / FIXED_FPS, 1)
75
 
 
76
  DEVICE_LABEL = (
77
  torch.cuda.get_device_name(torch.cuda.current_device()).lower()
78
  if torch.cuda.is_available() else str(device).lower()
 
158
  "形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
159
  )
160
 
161
+
162
  def resize_image(image: Image.Image) -> Image.Image:
163
  w, h = image.size
164
  if w == h:
 
194
 
195
 
196
  def extract_frames_from_video(video_path: str, duration_seconds: float) -> List[Image.Image]:
 
 
 
 
 
197
  cap = cv2.VideoCapture(video_path)
198
  fps = cap.get(cv2.CAP_PROP_FPS) or FIXED_FPS
199
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
200
  total_secs = total_frames / fps
201
 
202
+ n_buckets = max(1, int(duration_seconds))
203
  frames_out = []
204
  for i in range(n_buckets):
205
+ t = i + 0.5
206
  t = min(t, total_secs - 0.05)
207
  frame_idx = int(t * fps)
208
  cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
 
215
 
216
 
217
  def upscale_frames(pil_frames: List[Image.Image], run_id: str):
 
 
 
 
 
218
  results = []
219
  for i, frame in enumerate(pil_frames):
220
  label = f"Frame {i+1} (~{i+1}s)"
 
277
  generator=torch.Generator(device="cuda").manual_seed(current_seed),
278
  ).frames[0]
279
 
280
+ run_id = uuid.uuid4().hex
281
  vid_name = f"video_{run_id}.mp4"
282
  vid_path = OUTPUT_DIR / vid_name
283
  export_to_video(frames, str(vid_path), fps=FIXED_FPS)
284
 
 
285
  pil_frames = extract_frames_from_video(str(vid_path), duration_seconds)
286
  frame_results = upscale_frames(pil_frames, run_id)
287
 
 
308
  },
309
  ]
310
 
 
311
  @app.get("/example-file/{filename}")
312
  async def example_file(filename: str):
313
  path = EXAMPLES_DIR / filename
314
  if not path.exists():
315
  return JSONResponse({"error": f"Not found: {filename}"}, status_code=404)
316
  suffix = Path(filename).suffix.lower()
317
+ mt = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp"}.get(suffix, "image/jpeg")
318
  return FileResponse(path, media_type=mt)
319
 
320
 
 
344
 
345
  @app.post("/api/generate")
346
  async def generate_video_endpoint(
347
+ prompt: str = Form(...),
348
+ negative_prompt: str = Form(DEFAULT_NEGATIVE_PROMPT),
349
+ seed: str = Form("42"),
350
+ randomize_seed: str = Form("true"),
351
+ duration: str = Form("3.5"),
352
+ steps: str = Form("4"),
353
+ guidance_scale: str = Form("1.0"),
354
+ guidance_scale_2: str = Form("1.0"),
355
+ image: Optional[UploadFile] = File(None),
356
  ):
357
  temp_path = None
358
  try:
 
394
  if temp_path and os.path.exists(temp_path):
395
  os.remove(temp_path)
396
 
 
397
  @app.get("/", response_class=HTMLResponse)
398
  async def homepage(request: Request):
399
+ html = (BASE_DIR / "index.html").read_text(encoding="utf-8")
400
+
401
+ # Inject Python-side runtime values into the HTML placeholders
402
+ replacements = {
403
+ "[[EXAMPLES_JSON]]": json.dumps(get_example_items()),
404
+ "[[MIN_DURATION]]": str(MIN_DURATION),
405
+ "[[MAX_DURATION]]": str(MAX_DURATION),
406
+ "[[DEFAULT_PROMPT]]": DEFAULT_PROMPT,
407
+ "[[DEFAULT_NEG]]": DEFAULT_NEGATIVE_PROMPT,
408
+ "[[DEVICE_LABEL]]": DEVICE_LABEL,
409
+ }
410
+ for placeholder, value in replacements.items():
411
+ html = html.replace(placeholder, value)
412
+
413
+ return html
414
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
  if __name__ == "__main__":
417
  app.launch(show_error=True)