Download generate.py from Emma5099/music2dance: direct link, hf CLI and curl.
- Browser
- Download file 6.26 kB
-
https://huggingface.co/Emma5099/music2dance/resolve/main/generate.py
- Command line
-
hf download hf://Emma5099/music2dance/generate.py
-
curl -L -o generate.py https://huggingface.co/Emma5099/music2dance/resolve/main/generate.py
6.26 kB
| #!/usr/bin/env python3 | |
| """ | |
| Generate a dancing skeleton video from any audio file. | |
| Usage: | |
| python generate.py --audio song.wav --out dance.mp4 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import subprocess | |
| import librosa | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.animation as animation | |
| import numpy as np | |
| import soundfile as sf | |
| import torch | |
| from tqdm import tqdm | |
| from audio_features import AUDIO_SR, POSE_FPS, audio_to_features | |
| from inference import generate_poses, load_checkpoint, resolve_checkpoint | |
| SKELETON_EDGES = [ | |
| (0, 1), (1, 2), (2, 3), (3, 7), (0, 4), (4, 5), (5, 6), (6, 8), (9, 10), | |
| (11, 12), (11, 13), (13, 15), (12, 14), (14, 16), | |
| (11, 23), (12, 24), (23, 24), (23, 25), (24, 26), (25, 27), (26, 28), | |
| (27, 29), (28, 30), (29, 31), (30, 32), | |
| ] | |
| FACE = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} | |
| LEFT = {11, 13, 15, 23, 25, 27, 29, 31} | |
| RIGHT = {12, 14, 16, 24, 26, 28, 30, 32} | |
| def load_audio(path: str) -> np.ndarray: | |
| """Load any audio file as mono float32 at 32 kHz.""" | |
| data, sr = sf.read(path, dtype="float32", always_2d=True) | |
| mono = data.mean(axis=1) | |
| if sr != AUDIO_SR: | |
| mono = librosa.resample(mono, orig_sr=sr, target_sr=AUDIO_SR) | |
| return mono.astype(np.float32) | |
| def _segment_color(a: int, b: int) -> str: | |
| if a in FACE or b in FACE: | |
| return "#a0c4ff" | |
| if a in LEFT and b in LEFT: | |
| return "#ffd6a5" | |
| if a in RIGHT and b in RIGHT: | |
| return "#ffadad" | |
| return "#b9fbc0" | |
| def render_skeleton_video(poses_xyz: np.ndarray, title: str, tmp_path: str) -> None: | |
| """Render a silent skeleton animation (720×1080).""" | |
| T = poses_xyz.shape[0] | |
| vel = np.diff(poses_xyz, axis=0) | |
| energy = np.concatenate([[0], np.linalg.norm(vel, axis=-1).mean(axis=-1)]) | |
| energy = energy / (energy.max() + 1e-6) | |
| xmin = poses_xyz[:, :, 0].min() - 0.15 | |
| xmax = poses_xyz[:, :, 0].max() + 0.15 | |
| ymin = poses_xyz[:, :, 1].min() - 0.1 | |
| ymax = poses_xyz[:, :, 1].max() + 0.1 | |
| fig = plt.figure(figsize=(5.4, 8), facecolor="#0d1117") | |
| ax = fig.add_axes([0.05, 0.08, 0.90, 0.86]) | |
| ax.set_facecolor("#0d1117") | |
| ax.set_xlim(xmin, xmax) | |
| ax.set_ylim(-ymax, -ymin) | |
| ax.set_aspect("equal") | |
| ax.axis("off") | |
| fig.text(0.5, 0.97, title, color="#e6edf3", fontsize=11, | |
| ha="center", va="top", fontweight="bold") | |
| bar_ax = fig.add_axes([0.05, 0.02, 0.90, 0.04]) | |
| bar_ax.set_xlim(0, T) | |
| bar_ax.set_ylim(0, 1) | |
| bar_ax.axis("off") | |
| progress_bar = bar_ax.barh(0.5, 0, height=0.8, color="#3fb950", left=0) | |
| time_txt = bar_ax.text(T * 0.5, 0.5, "0.0 s", color="white", | |
| fontsize=7, ha="center", va="center") | |
| scat = ax.scatter([], [], s=18, zorder=4) | |
| lines = [ax.plot([], [], lw=2.2, solid_capstyle="round")[0] for _ in SKELETON_EDGES] | |
| def init(): | |
| scat.set_offsets(np.empty((0, 2))) | |
| for line in lines: | |
| line.set_data([], []) | |
| return [scat, *lines] | |
| def update(t): | |
| kpts = poses_xyz[t] | |
| xs, ys = kpts[:, 0], -kpts[:, 1] | |
| cols = [] | |
| for i in range(33): | |
| if i in FACE: | |
| cols.append("#a0c4ff") | |
| elif i in LEFT: | |
| cols.append("#ffd6a5") | |
| elif i in RIGHT: | |
| cols.append("#ffadad") | |
| else: | |
| cols.append("#b9fbc0") | |
| scat.set_offsets(np.c_[xs, ys]) | |
| scat.set_color(cols) | |
| for idx, (a, b) in enumerate(SKELETON_EDGES): | |
| lines[idx].set_data([xs[a], xs[b]], [ys[a], ys[b]]) | |
| lines[idx].set_color(_segment_color(a, b)) | |
| lines[idx].set_alpha(0.85 + 0.15 * energy[t]) | |
| progress_bar[0].set_width(t + 1) | |
| time_txt.set_text(f"{t / POSE_FPS:.1f} s / {T / POSE_FPS:.1f} s") | |
| return [scat, *lines, progress_bar[0], time_txt] | |
| ani = animation.FuncAnimation( | |
| fig, update, frames=T, init_func=init, | |
| interval=1000 / POSE_FPS, blit=True, | |
| ) | |
| writer = animation.FFMpegWriter( | |
| fps=POSE_FPS, bitrate=2000, | |
| extra_args=["-vcodec", "libx264", "-pix_fmt", "yuv420p"], | |
| ) | |
| ani.save(tmp_path, writer=writer, dpi=150) | |
| plt.close(fig) | |
| def mux_audio(video_path: str, audio_path: str, out_path: str, duration: float) -> None: | |
| """Combine silent video with the original audio track.""" | |
| subprocess.run([ | |
| "ffmpeg", "-y", | |
| "-i", video_path, | |
| "-i", audio_path, | |
| "-c:v", "copy", | |
| "-c:a", "aac", "-b:a", "192k", | |
| "-t", str(duration), | |
| "-shortest", | |
| out_path, | |
| ], check=True, capture_output=True) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Music → dance skeleton video") | |
| parser.add_argument("--audio", required=True, help="Input audio file") | |
| parser.add_argument("--checkpoint", default=None, help="Path to .pt weights") | |
| parser.add_argument("--out", default="dance.mp4", help="Output video path") | |
| args = parser.parse_args() | |
| os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| ckpt_path = resolve_checkpoint(args.checkpoint) | |
| print(f"Device: {device}") | |
| print(f"Loading audio: {args.audio}") | |
| waveform = load_audio(args.audio) | |
| duration = len(waveform) / AUDIO_SR | |
| print(f" Duration: {duration:.1f}s") | |
| print("Extracting audio features…") | |
| audio_feat = audio_to_features(waveform) | |
| print(f" {audio_feat.shape[0]} frames @ {POSE_FPS} fps") | |
| print(f"Loading model: {ckpt_path}") | |
| model, ckpt = load_checkpoint(ckpt_path, device) | |
| print(f" Epoch {ckpt['epoch']}") | |
| print("Generating poses…") | |
| poses_xyz = generate_poses( | |
| model, audio_feat, | |
| ckpt["x_mean"], ckpt["x_std"], | |
| ckpt["y_mean"], ckpt["y_std"], | |
| device, | |
| ) | |
| title = os.path.splitext(os.path.basename(args.audio))[0] | |
| tmp = args.out.replace(".mp4", "_silent.mp4") | |
| print("Rendering video…") | |
| render_skeleton_video(poses_xyz, title=title, tmp_path=tmp) | |
| print("Mixing audio…") | |
| mux_audio(tmp, args.audio, args.out, duration) | |
| os.remove(tmp) | |
| print(f"Done → {args.out}") | |
| if __name__ == "__main__": | |
| main() | |