bann commited on
Commit
2d2d6b8
·
1 Parent(s): 8baf391

feat: upgrade default model to Wan 2.2 MoE suite and configure Civitai API key

Browse files
Files changed (2) hide show
  1. README.md +7 -6
  2. app.py +54 -39
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Wan 2.1 Studio
3
  emoji: 🎬
4
  colorFrom: indigo
5
  colorTo: purple
@@ -8,17 +8,18 @@ sdk_version: 5.20.0
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
- short_description: Wan 2.1 Video Studio with Multi-LoRA and I2V/T2V
12
  suggested_hardware: zero-a10g
13
  ---
14
 
15
- # Wan 2.1 / 2.2 Video Studio (ZeroGPU + Multi-LoRA)
16
 
17
- A high-performance AI video generation studio running **Wan 2.1 / Wan 2.2** on **ZeroGPU**.
18
 
19
  ### Features:
20
- - 🖼️ **Image-to-Video (I2V)** and 📝 **Text-to-Video (T2V)** support.
 
21
  - ⚡ **ZeroGPU Acceleration**: Runs with dynamic GPU scheduling on A10G / H100 hardware.
22
- - 🧩 **Multi-LoRA Engine**: Load up to 2 custom LoRAs from Civitai, Hugging Face, or local files with Trigger Words.
23
  - 📐 **300-Hours Civitai Optimization**: Strict multiple-of-16 aspect ratios (832x480, 480x832, 1280x720, etc.) for clean motion.
24
  - 📖 **Built-in Prompt & Anatomical Motion Guide**: Designed with the official structured prompt standards.
 
1
  ---
2
+ title: Wan 2.2 Studio
3
  emoji: 🎬
4
  colorFrom: indigo
5
  colorTo: purple
 
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
+ short_description: Wan 2.2 Video Studio with Multi-LoRA and I2V/T2V
12
  suggested_hardware: zero-a10g
13
  ---
14
 
15
+ # Wan 2.2 Video Studio (ZeroGPU + Multi-LoRA + Base NSFW)
16
 
17
+ A high-performance AI video generation studio running **Wan 2.2 (MoE)** on **ZeroGPU**.
18
 
19
  ### Features:
20
+ - 🚀 **Wan 2.2 MoE Architecture**: Native Diffusers integration (`Wan2.2-I2V-A14B`, `Wan2.2-TI2V-5B`, `Wan2.2-T2V-A14B`).
21
+ - 🔞 **Base NSFW Adapter System**: Pre-loaded with `WAN General NSFW Model (Civitai 1307155)` to unlock full anatomical generation.
22
  - ⚡ **ZeroGPU Acceleration**: Runs with dynamic GPU scheduling on A10G / H100 hardware.
23
+ - 🧩 **Multi-LoRA Engine**: Load custom action and style LoRAs from Civitai, Hugging Face, or local files with Trigger Words.
24
  - 📐 **300-Hours Civitai Optimization**: Strict multiple-of-16 aspect ratios (832x480, 480x832, 1280x720, etc.) for clean motion.
25
  - 📖 **Built-in Prompt & Anatomical Motion Guide**: Designed with the official structured prompt standards.
app.py CHANGED
@@ -1,4 +1,4 @@
1
- """Wan 2.1 / 2.2 Space with ZeroGPU, Multi-LoRA (Civitai + HF), Presets, Trigger Words, and 300-Hours Optimizations."""
2
 
3
  from __future__ import annotations
4
 
@@ -52,7 +52,8 @@ from PIL import Image, ImageOps
52
  from diffusers.utils import export_to_video
53
  from safetensors import safe_open
54
 
55
- DEFAULT_MODEL_REPO = os.environ.get("WAN_MODEL_REPO", "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers")
 
56
  GPU_SIZE = os.environ.get("WAN_GPU_SIZE", "xlarge")
57
  MAX_GPU_DURATION = int(os.environ.get("WAN_MAX_GPU_DURATION", "300"))
58
  OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "wan-outputs")
@@ -85,7 +86,7 @@ NSFW_BASE_ADAPTERS = {
85
  }
86
 
87
  # =========================================================================
88
- # Preset Catalog for Wan 2.1 / 2.2 LoRAs (Actions, Motions & Styles)
89
  # =========================================================================
90
  LORA_PRESETS = {
91
  "None / Desativado": {
@@ -99,16 +100,16 @@ LORA_PRESETS = {
99
  "source": "https://civitai.com/models/1307155",
100
  "trigger_words": "nsfwsks, uncensored, naked, explicit",
101
  "default_strength": 1.0,
102
- "description": "LoRA Geral NSFW para Wan 2.1 / 2.2 (Civitai 1307155).",
103
  },
104
- "HMNSFW AIO V2 / hmmotion (Wan 2.1 / 2.2)": {
105
  "type": "civitai",
106
  "source": "https://civitai.com/api/download/models/3206518",
107
  "trigger_words": "hmmotion, missionary, side, fast, third-person side view, medium shot.",
108
  "default_strength": 0.5,
109
  "description": "LoRA All-in-One de anatomia e movimento realista (Civitai 2834417 / 3206518). Use força <= 0.5 com prompts descritivos.",
110
  },
111
- "Icy Twerk Pro Max (Wan 2.1 / 2.2)": {
112
  "type": "civitai",
113
  "source": "https://civitai.com/api/download/models/3201584",
114
  "trigger_words": "icytw3rk, twerking, booty shake, rhythmic hip movement, dynamic motion, bouncing buttocks, high quality",
@@ -265,7 +266,7 @@ def _validate_public_lora_url(url: str) -> str:
265
  def _download_lora_url(url: str, civitai_token: str = "") -> tuple[str, str]:
266
  import requests
267
 
268
- token = (civitai_token or os.environ.get("CIVITAI_API_KEY", "") or os.environ.get("CIVITAI_TOKEN", "")).strip()
269
 
270
  if "civitai." in url and token and "token=" not in url:
271
  sep = "&" if "?" in url else "?"
@@ -310,8 +311,7 @@ def _download_lora_url(url: str, civitai_token: str = "") -> tuple[str, str]:
310
  if response.status_code in (401, 403):
311
  raise gr.Error(
312
  "🔒 O Civitai bloqueou o download deste modelo (401 Unauthorized / NSFW). "
313
- "Gere um Civitai API Key em https://civitai.com/user/account-settings "
314
- "e cole no campo 'Civitai API Key' no painel do Space."
315
  )
316
  response.raise_for_status()
317
  total = 0
@@ -334,7 +334,7 @@ def _download_lora_url(url: str, civitai_token: str = "") -> tuple[str, str]:
334
  if "response" in locals() and response.status_code in (401, 403):
335
  raise gr.Error(
336
  "🔒 O Civitai bloqueou o download deste modelo (401 Unauthorized / NSFW). "
337
- "Gere um Civitai API Key em https://civitai.com/user/account-settings e cole no campo 'Civitai API Key'."
338
  ) from err
339
  raise
340
  raise ValueError("Too many redirects downloading LoRA.")
@@ -343,7 +343,7 @@ def _download_lora_url(url: str, civitai_token: str = "") -> tuple[str, str]:
343
  def resolve_single_lora(
344
  preset_type: str, custom_url: str, hf_repo: str, hf_file: str, local_file: str, strength: float, civitai_token: str = ""
345
  ) -> tuple[str | None, str, float]:
346
- """Resolves one LoRA file path, label and scale for Wan 2.1/2.2."""
347
  if float(strength) == 0.0 or preset_type in ("None / Desativado", "None", ""):
348
  return None, "None", 0.0
349
 
@@ -410,19 +410,31 @@ def load_pipeline(model_repo: str = DEFAULT_MODEL_REPO):
410
 
411
  started = time.time()
412
  try:
413
- from diffusers import WanImageToVideoPipeline, WanPipeline
414
 
415
  print(f"[wan] loading pipeline from {model_repo} ...", flush=True)
416
  if "I2V" in model_repo or "i2v" in model_repo:
417
- pipe = WanImageToVideoPipeline.from_pretrained(
418
- model_repo,
419
- torch_dtype=torch.bfloat16,
420
- )
 
 
 
 
 
 
421
  else:
422
- pipe = WanPipeline.from_pretrained(
423
- model_repo,
424
- torch_dtype=torch.bfloat16,
425
- )
 
 
 
 
 
 
426
 
427
  PIPE = pipe
428
  CURRENT_MODEL_REPO = model_repo
@@ -509,7 +521,7 @@ def _generate_video_gpu(
509
 
510
  try:
511
  with torch.inference_mode():
512
- if image_input is not None and ("I2V" in model_repo or "i2v" in model_repo):
513
  output = pipe(
514
  image=image_input,
515
  prompt=prompt,
@@ -585,6 +597,7 @@ def generate_video(
585
  if input_image is not None:
586
  processed_image = _fit_keyframe(input_image, width, height)
587
 
 
588
  lora_configs = []
589
  active_labels = []
590
 
@@ -594,13 +607,13 @@ def generate_video(
594
  if base_spec.get("source"):
595
  b_url = normalize_civitai_url(base_spec["source"])
596
  if b_url:
597
- b_path, b_label = _download_lora_url(b_url, civitai_api_key)
598
  lora_configs.append((b_path, float(nsfw_base_strength)))
599
  active_labels.append(f"🔞 NSFW Base: {nsfw_base_choice.split('(')[0].strip()} (@ {nsfw_base_strength:g})")
600
 
601
  # 2. Slot 1 LoRA
602
  l1_path, l1_label, l1_scale = resolve_single_lora(
603
- lora1_preset, lora1_custom_url, lora1_hf_repo, lora1_hf_file, lora1_local_file, lora1_strength, civitai_api_key
604
  )
605
  if l1_path and l1_scale > 0:
606
  lora_configs.append((l1_path, l1_scale))
@@ -608,7 +621,7 @@ def generate_video(
608
 
609
  # 3. Slot 2 LoRA
610
  l2_path, l2_label, l2_scale = resolve_single_lora(
611
- lora2_preset, lora2_custom_url, lora2_hf_repo, lora2_hf_file, lora2_local_file, lora2_strength, civitai_api_key
612
  )
613
  if l2_path and l2_scale > 0:
614
  lora_configs.append((l2_path, l2_scale))
@@ -728,17 +741,17 @@ all_preset_choices = (
728
  + (["Local File (loras/ folder)"] if get_local_loras() else [])
729
  )
730
 
731
- with gr.Blocks(css=custom_css, title="Wan 2.1 / 2.2 Video Studio") as app:
732
  gr.HTML(
733
  """
734
  <div class="header-card">
735
  <div style="margin-bottom: 12px;">
736
  <span class="badge badge-zerogpu">⚡ ZeroGPU</span>
737
- <span class="badge badge-model">🎬 Wan 2.1 / 2.2 (Diffusers)</span>
738
  <span class="badge badge-multilora">🧩 Base NSFW + Multi-LoRA Engine</span>
739
  </div>
740
- <h1>Wan 2.1 / 2.2 AI Video Studio</h1>
741
- <p>Image-to-Video (I2V) & Text-to-Video (T2V) com Adaptador NSFW Base, Multi-LoRA e Resoluções Múltiplas de 16.</p>
742
  </div>
743
  """
744
  )
@@ -759,7 +772,7 @@ with gr.Blocks(css=custom_css, title="Wan 2.1 / 2.2 Video Studio") as app:
759
  value="deformed, bad anatomy, extra limbs, blurry, low resolution, bad quality, distortion, censorship",
760
  )
761
 
762
- with gr.Accordion("📖 Guia & Modelos de Prompt HMNSFW / hmmotion (Wan 2.1 / 2.2)", open=False):
763
  gr.Markdown(
764
  """
765
  **Estrutura Recomendada pelo Guia de 300 Horas / Autor do LoRA (`hmmotion`)**:
@@ -800,7 +813,7 @@ with gr.Blocks(css=custom_css, title="Wan 2.1 / 2.2 Video Studio") as app:
800
  lora1_preset = gr.Dropdown(
801
  label="LoRA Slot 1 Preset / Fonte",
802
  choices=all_preset_choices,
803
- value="HMNSFW AIO V2 / hmmotion (Wan 2.1 / 2.2)",
804
  )
805
  with gr.Group(visible=False) as lora1_custom_url_grp:
806
  lora1_custom_url = gr.Textbox(
@@ -816,7 +829,7 @@ with gr.Blocks(css=custom_css, title="Wan 2.1 / 2.2 Video Studio") as app:
816
 
817
  lora1_trigger_display = gr.Textbox(
818
  label="Trigger Words",
819
- value=LORA_PRESETS.get("HMNSFW AIO V2 / hmmotion (Wan 2.1 / 2.2)", {}).get("trigger_words", ""),
820
  interactive=False,
821
  )
822
  add_lora1_triggers_btn = gr.Button("📋 Inserir Trigger Words no Prompt", elem_classes=["trigger-btn"])
@@ -867,12 +880,13 @@ with gr.Blocks(css=custom_css, title="Wan 2.1 / 2.2 Video Studio") as app:
867
  model_choice = gr.Dropdown(
868
  label="Modelo Base Wan",
869
  choices=[
870
- ("Wan 2.1 I2V 14B 480P (Recomendado ZeroGPU)", "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"),
871
- ("Wan 2.1 I2V 14B 720P (Alta Definição)", "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers"),
872
- ("Wan 2.1 T2V 1.3B (Ultra Rápido)", "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"),
873
- ("Wan 2.1 T2V 14B (Texto para Vídeo)", "Wan-AI/Wan2.1-T2V-14B-Diffusers"),
 
874
  ],
875
- value="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
876
  )
877
  canvas = gr.Dropdown(
878
  label="Proporção & Resolução (Múltiplos de 16)",
@@ -917,12 +931,13 @@ with gr.Blocks(css=custom_css, title="Wan 2.1 / 2.2 Video Studio") as app:
917
  randomize_seed = gr.Checkbox(label="🎲 Randomizar Seed", value=True)
918
 
919
  civitai_api_key = gr.Textbox(
920
- label="🔑 Civitai API Key (Opcional)",
921
- placeholder="Cole seu token Civitai se baixar modelos restritos/NSFW direto do civitai.com (ou adicione CIVITAI_API_KEY no Space Secrets)",
 
922
  type="password",
923
  )
924
 
925
- generate_btn = gr.Button("🚀 Gerar Vídeo Wan 2.1", variant="primary", elem_classes=["generate-btn"])
926
 
927
  with gr.Column(scale=6):
928
  output_video = gr.Video(label="Vídeo Gerado", autoplay=True, loop=True)
 
1
+ """Wan 2.2 Space with ZeroGPU, Multi-LoRA (Civitai + HF), Presets, Trigger Words, and 300-Hours Optimizations."""
2
 
3
  from __future__ import annotations
4
 
 
52
  from diffusers.utils import export_to_video
53
  from safetensors import safe_open
54
 
55
+ DEFAULT_CIVITAI_KEY = "50a9e1bd474c03b856070a7272d8015c"
56
+ DEFAULT_MODEL_REPO = os.environ.get("WAN_MODEL_REPO", "Wan-AI/Wan2.2-I2V-A14B-480P-Diffusers")
57
  GPU_SIZE = os.environ.get("WAN_GPU_SIZE", "xlarge")
58
  MAX_GPU_DURATION = int(os.environ.get("WAN_MAX_GPU_DURATION", "300"))
59
  OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "wan-outputs")
 
86
  }
87
 
88
  # =========================================================================
89
+ # Preset Catalog for Wan 2.2 LoRAs (Actions, Motions & Styles)
90
  # =========================================================================
91
  LORA_PRESETS = {
92
  "None / Desativado": {
 
100
  "source": "https://civitai.com/models/1307155",
101
  "trigger_words": "nsfwsks, uncensored, naked, explicit",
102
  "default_strength": 1.0,
103
+ "description": "LoRA Geral NSFW para Wan 2.2 (Civitai 1307155).",
104
  },
105
+ "HMNSFW AIO V2 / hmmotion (Wan 2.2)": {
106
  "type": "civitai",
107
  "source": "https://civitai.com/api/download/models/3206518",
108
  "trigger_words": "hmmotion, missionary, side, fast, third-person side view, medium shot.",
109
  "default_strength": 0.5,
110
  "description": "LoRA All-in-One de anatomia e movimento realista (Civitai 2834417 / 3206518). Use força <= 0.5 com prompts descritivos.",
111
  },
112
+ "Icy Twerk Pro Max (Wan 2.2)": {
113
  "type": "civitai",
114
  "source": "https://civitai.com/api/download/models/3201584",
115
  "trigger_words": "icytw3rk, twerking, booty shake, rhythmic hip movement, dynamic motion, bouncing buttocks, high quality",
 
266
  def _download_lora_url(url: str, civitai_token: str = "") -> tuple[str, str]:
267
  import requests
268
 
269
+ token = (civitai_token or os.environ.get("CIVITAI_API_KEY", "") or os.environ.get("CIVITAI_TOKEN", "") or DEFAULT_CIVITAI_KEY).strip()
270
 
271
  if "civitai." in url and token and "token=" not in url:
272
  sep = "&" if "?" in url else "?"
 
311
  if response.status_code in (401, 403):
312
  raise gr.Error(
313
  "🔒 O Civitai bloqueou o download deste modelo (401 Unauthorized / NSFW). "
314
+ "Verifique sua Civitai API Key nas configurações."
 
315
  )
316
  response.raise_for_status()
317
  total = 0
 
334
  if "response" in locals() and response.status_code in (401, 403):
335
  raise gr.Error(
336
  "🔒 O Civitai bloqueou o download deste modelo (401 Unauthorized / NSFW). "
337
+ "Verifique sua Civitai API Key nas configurações."
338
  ) from err
339
  raise
340
  raise ValueError("Too many redirects downloading LoRA.")
 
343
  def resolve_single_lora(
344
  preset_type: str, custom_url: str, hf_repo: str, hf_file: str, local_file: str, strength: float, civitai_token: str = ""
345
  ) -> tuple[str | None, str, float]:
346
+ """Resolves one LoRA file path, label and scale for Wan 2.2."""
347
  if float(strength) == 0.0 or preset_type in ("None / Desativado", "None", ""):
348
  return None, "None", 0.0
349
 
 
410
 
411
  started = time.time()
412
  try:
413
+ from diffusers import WanImageToVideoPipeline, WanPipeline, AutoPipelineForImage2Video, AutoPipelineForText2Video
414
 
415
  print(f"[wan] loading pipeline from {model_repo} ...", flush=True)
416
  if "I2V" in model_repo or "i2v" in model_repo:
417
+ try:
418
+ pipe = WanImageToVideoPipeline.from_pretrained(
419
+ model_repo,
420
+ torch_dtype=torch.bfloat16,
421
+ )
422
+ except Exception:
423
+ pipe = AutoPipelineForImage2Video.from_pretrained(
424
+ model_repo,
425
+ torch_dtype=torch.bfloat16,
426
+ )
427
  else:
428
+ try:
429
+ pipe = WanPipeline.from_pretrained(
430
+ model_repo,
431
+ torch_dtype=torch.bfloat16,
432
+ )
433
+ except Exception:
434
+ pipe = AutoPipelineForText2Video.from_pretrained(
435
+ model_repo,
436
+ torch_dtype=torch.bfloat16,
437
+ )
438
 
439
  PIPE = pipe
440
  CURRENT_MODEL_REPO = model_repo
 
521
 
522
  try:
523
  with torch.inference_mode():
524
+ if image_input is not None and ("I2V" in model_repo or "i2v" in model_repo or "TI2V" in model_repo):
525
  output = pipe(
526
  image=image_input,
527
  prompt=prompt,
 
597
  if input_image is not None:
598
  processed_image = _fit_keyframe(input_image, width, height)
599
 
600
+ token_to_use = (civitai_api_key or DEFAULT_CIVITAI_KEY).strip()
601
  lora_configs = []
602
  active_labels = []
603
 
 
607
  if base_spec.get("source"):
608
  b_url = normalize_civitai_url(base_spec["source"])
609
  if b_url:
610
+ b_path, b_label = _download_lora_url(b_url, token_to_use)
611
  lora_configs.append((b_path, float(nsfw_base_strength)))
612
  active_labels.append(f"🔞 NSFW Base: {nsfw_base_choice.split('(')[0].strip()} (@ {nsfw_base_strength:g})")
613
 
614
  # 2. Slot 1 LoRA
615
  l1_path, l1_label, l1_scale = resolve_single_lora(
616
+ lora1_preset, lora1_custom_url, lora1_hf_repo, lora1_hf_file, lora1_local_file, lora1_strength, token_to_use
617
  )
618
  if l1_path and l1_scale > 0:
619
  lora_configs.append((l1_path, l1_scale))
 
621
 
622
  # 3. Slot 2 LoRA
623
  l2_path, l2_label, l2_scale = resolve_single_lora(
624
+ lora2_preset, lora2_custom_url, lora2_hf_repo, lora2_hf_file, lora2_local_file, lora2_strength, token_to_use
625
  )
626
  if l2_path and l2_scale > 0:
627
  lora_configs.append((l2_path, l2_scale))
 
741
  + (["Local File (loras/ folder)"] if get_local_loras() else [])
742
  )
743
 
744
+ with gr.Blocks(css=custom_css, title="Wan 2.2 Video Studio") as app:
745
  gr.HTML(
746
  """
747
  <div class="header-card">
748
  <div style="margin-bottom: 12px;">
749
  <span class="badge badge-zerogpu">⚡ ZeroGPU</span>
750
+ <span class="badge badge-model">🎬 Wan 2.2 (MoE Diffusers)</span>
751
  <span class="badge badge-multilora">🧩 Base NSFW + Multi-LoRA Engine</span>
752
  </div>
753
+ <h1>Wan 2.2 AI Video Studio</h1>
754
+ <p>Image-to-Video (I2V) & Text-to-Video (T2V) com Arquitetura MoE Wan 2.2, Adaptador NSFW Base e Multi-LoRA.</p>
755
  </div>
756
  """
757
  )
 
772
  value="deformed, bad anatomy, extra limbs, blurry, low resolution, bad quality, distortion, censorship",
773
  )
774
 
775
+ with gr.Accordion("📖 Guia & Modelos de Prompt HMNSFW / hmmotion (Wan 2.2)", open=False):
776
  gr.Markdown(
777
  """
778
  **Estrutura Recomendada pelo Guia de 300 Horas / Autor do LoRA (`hmmotion`)**:
 
813
  lora1_preset = gr.Dropdown(
814
  label="LoRA Slot 1 Preset / Fonte",
815
  choices=all_preset_choices,
816
+ value="HMNSFW AIO V2 / hmmotion (Wan 2.2)",
817
  )
818
  with gr.Group(visible=False) as lora1_custom_url_grp:
819
  lora1_custom_url = gr.Textbox(
 
829
 
830
  lora1_trigger_display = gr.Textbox(
831
  label="Trigger Words",
832
+ value=LORA_PRESETS.get("HMNSFW AIO V2 / hmmotion (Wan 2.2)", {}).get("trigger_words", ""),
833
  interactive=False,
834
  )
835
  add_lora1_triggers_btn = gr.Button("📋 Inserir Trigger Words no Prompt", elem_classes=["trigger-btn"])
 
880
  model_choice = gr.Dropdown(
881
  label="Modelo Base Wan",
882
  choices=[
883
+ ("Wan 2.2 I2V A14B 480P (MoE - Recomendado ZeroGPU)", "Wan-AI/Wan2.2-I2V-A14B-480P-Diffusers"),
884
+ ("Wan 2.2 I2V A14B 720P (MoE Alta Definição)", "Wan-AI/Wan2.2-I2V-A14B-720P-Diffusers"),
885
+ ("Wan 2.2 TI2V 5B (Híbrido Ultra Rápido)", "Wan-AI/Wan2.2-TI2V-5B-Diffusers"),
886
+ ("Wan 2.2 T2V A14B (Texto para Vídeo MoE)", "Wan-AI/Wan2.2-T2V-A14B-Diffusers"),
887
+ ("Wan 2.1 I2V 14B 480P (Versão Anterior)", "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"),
888
  ],
889
+ value="Wan-AI/Wan2.2-I2V-A14B-480P-Diffusers",
890
  )
891
  canvas = gr.Dropdown(
892
  label="Proporção & Resolução (Múltiplos de 16)",
 
931
  randomize_seed = gr.Checkbox(label="🎲 Randomizar Seed", value=True)
932
 
933
  civitai_api_key = gr.Textbox(
934
+ label="🔑 Civitai API Key",
935
+ value=DEFAULT_CIVITAI_KEY,
936
+ placeholder="Chave de API do Civitai configurada",
937
  type="password",
938
  )
939
 
940
+ generate_btn = gr.Button("🚀 Gerar Vídeo Wan 2.2", variant="primary", elem_classes=["generate-btn"])
941
 
942
  with gr.Column(scale=6):
943
  output_video = gr.Video(label="Vídeo Gerado", autoplay=True, loop=True)