import os import sys import base64 import time from pathlib import Path # Dynamic monkeypatch for gradio_client schema parsing bug (TypeError: argument of type 'bool' is not iterable) try: import gradio_client.utils _orig_json_schema_to_python_type = gradio_client.utils._json_schema_to_python_type def patched_json_schema_to_python_type(schema, defs=None): if isinstance(schema, bool): return "Any" try: return _orig_json_schema_to_python_type(schema, defs) except Exception: return "Any" _orig_get_type = getattr(gradio_client.utils, "get_type", None) if _orig_get_type: def patched_get_type(schema): if isinstance(schema, bool): return "Any" try: return _orig_get_type(schema) except Exception: return "Any" gradio_client.utils.get_type = patched_get_type gradio_client.utils._json_schema_to_python_type = patched_json_schema_to_python_type print("Successfully patched gradio_client schema parsing helper.") except Exception as e: print(f"gradio_client patch failed: {e}") import gradio as gr from PIL import Image, ImageFile from loguru import logger from dotenv import load_dotenv # Set paths before importing task_history and local_inference os.environ["TASK_CACHE_DIR"] = str(Path(__file__).resolve().parent / "task_cache") os.environ["TASK_HISTORY_PATH"] = str(Path(__file__).resolve().parent / "task_history.json") # Import the xfuser mock first to register it in sys.modules import xfuser # Import spaces if available (Hugging Face ZeroGPU environment) try: import spaces except Exception: # Dummy decorator for local running/testing class spaces: @staticmethod def GPU(duration=120): def decorator(fn): return fn return decorator from local_inference import ( generate_dance_video, truncate_audio, crop_and_resize, FPS ) import task_history # Setup directories TASK_CACHE_DIR = Path(os.environ["TASK_CACHE_DIR"]) TASK_CACHE_DIR.mkdir(parents=True, exist_ok=True) # Load env vars (including HF_TOKEN) load_dotenv() # The HF_TOKEN should be configured in Hugging Face Space settings (secrets) # and is read directly from the environment. # Pre-download models on startup so the Space is ready for inference from huggingface_hub import snapshot_download try: print("Pre-downloading model weights from Hugging Face...") snapshot_download( repo_id="Wan-AI/Wan-Dancer-14B", local_dir="./models/Wan-AI/Wan-Dancer-14B", token=os.environ.get("HF_TOKEN") ) print("Pre-download completed successfully.") except Exception as e: print(f"Error pre-downloading models: {e}") finally: if "HF_TOKEN" in os.environ: del os.environ["HF_TOKEN"] print("Cleared HF_TOKEN from environment variables to prevent ZeroGPU quota override.") try: from local_inference import get_or_init_pipeline print("Pre-loading models into CPU memory...") get_or_init_pipeline(local_model_path="./models") print("Pre-loading models completed successfully.") except Exception as e: print(f"Error pre-loading models: {e}") DANCE_GENRES = [ ("Chinese Classical", "chinese_classical"), ("K-Pop", "k_pop"), ("Street Dance", "street"), ("Tap Dance", "tap"), ("Latin Dance", "latin"), ] DANCE_GENRE_LABELS = {v: k for k, v in DANCE_GENRES} HISTORY_PAGE_SIZE = 20 def _image_to_thumbnail_base64(image_path: str, size: int = 64) -> str | None: if not image_path or not os.path.isfile(image_path): return None try: img = Image.open(image_path) img = img.convert("RGB") img.thumbnail((size, size), Image.Resampling.LANCZOS) import io buf = io.BytesIO() img.save(buf, format="JPEG", quality=75) return base64.b64encode(buf.getvalue()).decode("ascii") except Exception: return None def _render_history_page(tasks: list, page: int) -> tuple[str, str]: total = len(tasks) total_pages = max(1, (total + HISTORY_PAGE_SIZE - 1) // HISTORY_PAGE_SIZE) page = max(0, min(page, total_pages - 1)) start = page * HISTORY_PAGE_SIZE end = start + HISTORY_PAGE_SIZE page_tasks = tasks[start:end] if not page_tasks: return "No task history.", "Page 0/0, 0 total" lines = [ "| Thumbnail | task_id | Status | Genre | Created | Result Video |", "| --- | --- | --- | --- | --- | --- |", ] for t in page_tasks: img_path = t.get("image_local_path") or "" thumb_b64 = _image_to_thumbnail_base64(img_path) if thumb_b64: thumb_cell = f'' else: thumb_cell = "—" task_id = (t.get("task_id") or "").replace("|", "\\|") status = (t.get("status") or "—").replace("|", "\\|") genre_val = t.get("dance_genres") or "" genre_label = DANCE_GENRE_LABELS.get(genre_val, genre_val or "—").replace("|", "\\|") # Format created_at created = t.get("created_at") or "" if created: try: from datetime import datetime dt = datetime.fromisoformat(created.replace("Z", "+00:00")) created = dt.strftime("%Y-%m-%d %H:%M") except Exception: created = created[:16] else: created = "—" video_local = (t.get("video_local_path") or "").strip() if video_local and os.path.isfile(video_local): video_cell = f"[View Video](/gradio_api/file={video_local})" else: video_cell = "—" lines.append(f"| {thumb_cell} | {task_id} | {status} | {genre_label} | {created} | {video_cell} |") page_info = f"Page {page + 1}/{total_pages}, {total} total" return "\n".join(lines), page_info def refresh_history(page: int = 0): tasks = task_history.load_history() md, info = _render_history_page(tasks, page) total_pages = max(1, (len(tasks) + HISTORY_PAGE_SIZE - 1) // HISTORY_PAGE_SIZE) current_page = max(0, min(page, total_pages - 1)) return md, info, current_page def history_prev_page(current_page: int): return refresh_history(max(0, current_page - 1)) def history_next_page(current_page: int): return refresh_history(current_page + 1) # ZeroGPU decorated entry handler @spaces.GPU(duration=90) def run_wan_dance( image_file, audio_file, dance_genre_value, max_audio_duration_value, audio_start_sec_value, steps_value, cfg_value, progress=gr.Progress(track_tqdm=True), request: gr.Request = None ): # Decode crop image if base64 data URL if image_file and isinstance(image_file, str) and image_file.startswith("data:image"): try: header, b64data = image_file.split(",", 1) import tempfile tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png", dir=str(TASK_CACHE_DIR)) tmp.write(base64.b64decode(b64data)) tmp.close() image_file = tmp.name logger.info("Decoded crop image: {}", image_file) except Exception as e: logger.error("Decoding base64 image failed: {}", e) return None, f"Image decoding failed: {e}", gr.skip(), gr.skip(), gr.skip() if not image_file: return None, "Please upload and crop a portrait image (9:16 vertical).", gr.skip(), gr.skip(), gr.skip() if not audio_file: return None, "Please upload a music file.", gr.skip(), gr.skip(), gr.skip() # Truncate audio max_dur = int(max_audio_duration_value) if max_audio_duration_value else 10 start_sec = float(audio_start_sec_value) if audio_start_sec_value else 0.0 original_audio = audio_file audio_file = truncate_audio(audio_file, max_dur, start_sec) if audio_file != original_audio: logger.info("Audio truncated to {}s (starting at {}s): {}", max_dur, start_sec, audio_file) steps = int(steps_value) if steps_value else 24 cfg = float(cfg_value) if cfg_value else 5.0 task_id = f"task_{int(time.time())}" # Setup local cache copies for task history img_ext = Path(image_file).suffix or ".jpg" aud_ext = Path(audio_file).suffix or ".mp3" # Save files to task cache folder task_sub_dir = TASK_CACHE_DIR / task_id task_sub_dir.mkdir(parents=True, exist_ok=True) import shutil image_local_path = str(task_sub_dir / f"image{img_ext}") audio_local_path = str(task_sub_dir / f"audio{aud_ext}") shutil.copy2(image_file, image_local_path) shutil.copy2(audio_file, audio_local_path) try: # Run local generation on ZeroGPU video_output = generate_dance_video( image_path=image_local_path, music_path=audio_local_path, genre=dance_genre_value, output_folder=str(TASK_CACHE_DIR), local_model_path="./models", seed=0, height=1280, width=720, steps=steps, cfg=cfg, progress=progress ) # Save output in local task sub-directory video_local_path = str(task_sub_dir / "video.mp4") shutil.move(video_output, video_local_path) # Save to history task_history.add_task( task_id=task_id, image_url="", audio_url="", status="SUCCEEDED", result_video_url=None, dance_genres=dance_genre_value, image_local_path=image_local_path, audio_local_path=audio_local_path, owner_id="" ) task_history.update_task(task_id, status="SUCCEEDED", video_local_path=video_local_path) # Reload history Markdown md, info, _ = refresh_history(0) return video_local_path, "Generation succeeded!", md, info, 0 except Exception as e: logger.exception("Inference failed") task_history.add_task( task_id=task_id, image_url="", audio_url="", status="FAILED", message=str(e), dance_genres=dance_genre_value, image_local_path=image_local_path, audio_local_path=audio_local_path, owner_id="" ) md, info, _ = refresh_history(0) return None, f"Generation failed: {e}", md, info, 0 def build_ui(): with gr.Blocks( title="Wan-dancer", theme=gr.themes.Soft( font=[gr.themes.GoogleFont("Source Sans Pro"), "Arial", "sans-serif"], ), css=""" .gradio-container {max-width: none !important;} #logo-banner { background: #ffffff !important; padding: 16px 0 !important; border-radius: 8px; } """, ) as demo: # Logo Banner try: _logo_path = Path(__file__).resolve().parent / "logo.png" _logo_b64 = base64.b64encode(_logo_path.read_bytes()).decode() _logo_html = ( '
' f'Wan-dancer' '
' ) except Exception as _e: _logo_html = '

Wan-dancer

' gr.HTML(_logo_html, elem_id="logo-banner") gr.Markdown( "
\n\n" "[GitHub](https://github.com/Wan-Video/Wan-Dancer) · " "[Hugging Face Model](https://huggingface.co/Wan-AI/Wan-Dancer-14B)\n\n" "
" ) gr.Markdown( "**Instructions**\n\n" "1. Upload a portrait image (9:16 vertical, full-body/half-body facing front) and a music file (wav/mp3, 10s-30s).\n" "2. Select a dance genre and customize generation parameters if desired, then click **Generate**.\n" "3. **ZeroGPU Optimization**: To stay within Hugging Face ZeroGPU time limits (120s max), keep **Inference Steps around 12-15** and **Max Audio Duration around 10-15s**." ) with gr.Row(equal_height=False): with gr.Column(scale=1): gr.Markdown("### Input") with gr.Group(): image_in = gr.Image( type="filepath", label="Portrait Image (will be auto-cropped to 9:16 vertical)", ) audio_in = gr.Audio( type="filepath", label="Music (wav or mp3)", sources=["upload"], ) max_audio_duration = gr.Slider( minimum=10, maximum=30, value=10, step=1, label="Max Audio Duration (seconds)", info="Video generation is computed sequentially in 5-second segments. Standard length is 10s.", ) audio_start_sec = gr.Slider( minimum=0, maximum=120, value=0, step=1, label="Audio Start Time (seconds)", info="Choose where in the song the dance should start (e.g. when the beat drops).", ) dance_genre = gr.Dropdown( choices=[(f"{label} ({v})", v) for label, v in DANCE_GENRES], value="chinese_classical", label="Dance Genre", ) with gr.Accordion("Parameters", open=False): with gr.Group(): with gr.Row(): steps = gr.Slider( minimum=10, maximum=50, value=15, step=1, label="Inference Steps", info="Number of denoising steps. Default is 15." ) cfg = gr.Slider( minimum=1.0, maximum=15.0, value=5.0, step=0.5, label="Classifier-free Guidance Scale", info="Guidance scale. Default is 5.0." ) submit_btn = gr.Button("Generate", variant="primary") with gr.Column(scale=1): gr.Markdown("### Result") video_out = gr.Video(label="Generated Video") msg_out = gr.Textbox( label="Status / Log", lines=4, interactive=False, ) # ==================== My Tasks ==================== with gr.Accordion("My Tasks", open=True): history_page_state = gr.State(value=0) history_markdown = gr.Markdown(value="") with gr.Row(): prev_page_btn = gr.Button("⬅ Prev", scale=1) page_info = gr.Textbox(value="", interactive=False, show_label=False, scale=2) next_page_btn = gr.Button("Next ➡", scale=1) refresh_history_btn = gr.Button("Refresh", scale=1) history_outputs = [history_markdown, page_info, history_page_state] demo.load( fn=refresh_history, inputs=[history_page_state], outputs=history_outputs, ) refresh_history_btn.click( fn=refresh_history, inputs=[history_page_state], outputs=history_outputs, ) prev_page_btn.click( fn=history_prev_page, inputs=[history_page_state], outputs=history_outputs, ) next_page_btn.click( fn=history_next_page, inputs=[history_page_state], outputs=history_outputs, ) # ==================== Examples (示例) ==================== _samples_dir = Path(__file__).resolve().parent / "samples" gr.Examples( examples=[ [str(_samples_dir / "1/image.png"), str(_samples_dir / "1/audio.mp3"), "street"], [str(_samples_dir / "2/image.jpeg"), str(_samples_dir / "2/audio.wav"), "chinese_classical"], ], inputs=[image_in, audio_in, dance_genre], label="Examples" ) submit_btn.click( fn=run_wan_dance, inputs=[image_in, audio_in, dance_genre, max_audio_duration, audio_start_sec, steps, cfg], outputs=[video_out, msg_out, history_markdown, page_info, history_page_state], ) return demo def main(): demo = build_ui() _samples_dir = str(Path(__file__).resolve().parent / "samples") demo.launch( allowed_paths=[str(TASK_CACHE_DIR), _samples_dir], ) if __name__ == "__main__": main()