Commit ·
5ac2b1b
1
Parent(s): dc36ee1
feat: add MuseTalk fast_engine integration and model download script
Browse files- Created server/fast_engine.py with MuseTalk integration for lip-sync avatar
- Added scripts/download_models.sh to download all required models from HuggingFace
- Added assets/avatar.png placeholder image
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- assets/avatar.png +0 -0
- scripts/download_models.sh +171 -0
- server/fast_engine.py +487 -0
assets/avatar.png
ADDED
|
|
scripts/download_models.sh
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# Download MuseTalk models from HuggingFace
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
set -e
|
| 7 |
+
|
| 8 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 9 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 10 |
+
MODELS_DIR="${PROJECT_DIR}/models"
|
| 11 |
+
|
| 12 |
+
GREEN='\033[0;32m'
|
| 13 |
+
CYAN='\033[0;36m'
|
| 14 |
+
YELLOW='\033[1;33m'
|
| 15 |
+
NC='\033[0m'
|
| 16 |
+
|
| 17 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 18 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 19 |
+
warn() { echo -e "${YELLOW}!${NC} $1"; }
|
| 20 |
+
|
| 21 |
+
echo ""
|
| 22 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 23 |
+
echo "║ MUSETALK - MODEL DOWNLOAD ║"
|
| 24 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 25 |
+
echo ""
|
| 26 |
+
|
| 27 |
+
# Check for huggingface-cli
|
| 28 |
+
if ! command -v huggingface-cli &> /dev/null; then
|
| 29 |
+
echo "Installing huggingface_hub..."
|
| 30 |
+
pip install huggingface_hub -q
|
| 31 |
+
fi
|
| 32 |
+
|
| 33 |
+
# Create directories
|
| 34 |
+
mkdir -p "${MODELS_DIR}/avatar/musetalk"
|
| 35 |
+
mkdir -p "${MODELS_DIR}/avatar/vae"
|
| 36 |
+
mkdir -p "${MODELS_DIR}/whisper"
|
| 37 |
+
mkdir -p "${MODELS_DIR}/dwpose"
|
| 38 |
+
mkdir -p "${MODELS_DIR}/face_parse"
|
| 39 |
+
|
| 40 |
+
# ============================================================
|
| 41 |
+
# 1. MUSETALK MODELS
|
| 42 |
+
# ============================================================
|
| 43 |
+
header "[1/5] Downloading MuseTalk UNet..."
|
| 44 |
+
|
| 45 |
+
if [ -f "${MODELS_DIR}/avatar/musetalk/pytorch_model.bin" ]; then
|
| 46 |
+
success "MuseTalk UNet already exists"
|
| 47 |
+
else
|
| 48 |
+
huggingface-cli download TMElyralab/MuseTalk \
|
| 49 |
+
--include "models/musetalk/*" \
|
| 50 |
+
--local-dir "${MODELS_DIR}/temp_musetalk" \
|
| 51 |
+
--local-dir-use-symlinks False
|
| 52 |
+
|
| 53 |
+
# Move files to correct location
|
| 54 |
+
if [ -d "${MODELS_DIR}/temp_musetalk/models/musetalk" ]; then
|
| 55 |
+
cp -r "${MODELS_DIR}/temp_musetalk/models/musetalk/"* "${MODELS_DIR}/avatar/musetalk/"
|
| 56 |
+
rm -rf "${MODELS_DIR}/temp_musetalk"
|
| 57 |
+
success "MuseTalk UNet downloaded"
|
| 58 |
+
else
|
| 59 |
+
warn "MuseTalk download may have failed"
|
| 60 |
+
fi
|
| 61 |
+
fi
|
| 62 |
+
|
| 63 |
+
# ============================================================
|
| 64 |
+
# 2. VAE (SD-VAE-FT-MSE)
|
| 65 |
+
# ============================================================
|
| 66 |
+
header "[2/5] Downloading VAE..."
|
| 67 |
+
|
| 68 |
+
if [ -f "${MODELS_DIR}/avatar/vae/config.json" ]; then
|
| 69 |
+
success "VAE already exists"
|
| 70 |
+
else
|
| 71 |
+
huggingface-cli download stabilityai/sd-vae-ft-mse \
|
| 72 |
+
--local-dir "${MODELS_DIR}/avatar/vae" \
|
| 73 |
+
--local-dir-use-symlinks False
|
| 74 |
+
|
| 75 |
+
success "VAE downloaded"
|
| 76 |
+
fi
|
| 77 |
+
|
| 78 |
+
# ============================================================
|
| 79 |
+
# 3. WHISPER
|
| 80 |
+
# ============================================================
|
| 81 |
+
header "[3/5] Downloading Whisper..."
|
| 82 |
+
|
| 83 |
+
if [ -f "${MODELS_DIR}/whisper/tiny.pt" ]; then
|
| 84 |
+
success "Whisper already exists"
|
| 85 |
+
else
|
| 86 |
+
# Download from OpenAI
|
| 87 |
+
wget -q -O "${MODELS_DIR}/whisper/tiny.pt" \
|
| 88 |
+
"https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt" \
|
| 89 |
+
|| curl -s -o "${MODELS_DIR}/whisper/tiny.pt" \
|
| 90 |
+
"https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt"
|
| 91 |
+
|
| 92 |
+
if [ -f "${MODELS_DIR}/whisper/tiny.pt" ]; then
|
| 93 |
+
success "Whisper downloaded"
|
| 94 |
+
else
|
| 95 |
+
warn "Whisper download failed"
|
| 96 |
+
fi
|
| 97 |
+
fi
|
| 98 |
+
|
| 99 |
+
# ============================================================
|
| 100 |
+
# 4. DWPOSE
|
| 101 |
+
# ============================================================
|
| 102 |
+
header "[4/5] Downloading DWPose..."
|
| 103 |
+
|
| 104 |
+
if [ -f "${MODELS_DIR}/dwpose/dw-ll_ucoco_384.pth" ]; then
|
| 105 |
+
success "DWPose already exists"
|
| 106 |
+
else
|
| 107 |
+
huggingface-cli download yzd-v/DWPose \
|
| 108 |
+
--include "dw-ll_ucoco_384.pth" \
|
| 109 |
+
--local-dir "${MODELS_DIR}/dwpose" \
|
| 110 |
+
--local-dir-use-symlinks False
|
| 111 |
+
|
| 112 |
+
success "DWPose downloaded"
|
| 113 |
+
fi
|
| 114 |
+
|
| 115 |
+
# ============================================================
|
| 116 |
+
# 5. FACE PARSING
|
| 117 |
+
# ============================================================
|
| 118 |
+
header "[5/5] Downloading Face Parsing models..."
|
| 119 |
+
|
| 120 |
+
if [ -f "${MODELS_DIR}/face_parse/79999_iter.pth" ]; then
|
| 121 |
+
success "Face parsing already exists"
|
| 122 |
+
else
|
| 123 |
+
# Download from MuseTalk repo
|
| 124 |
+
huggingface-cli download TMElyralab/MuseTalk \
|
| 125 |
+
--include "models/face-parse-bisenet/*" \
|
| 126 |
+
--local-dir "${MODELS_DIR}/temp_fp" \
|
| 127 |
+
--local-dir-use-symlinks False
|
| 128 |
+
|
| 129 |
+
if [ -d "${MODELS_DIR}/temp_fp/models/face-parse-bisenet" ]; then
|
| 130 |
+
cp -r "${MODELS_DIR}/temp_fp/models/face-parse-bisenet/"* "${MODELS_DIR}/face_parse/"
|
| 131 |
+
rm -rf "${MODELS_DIR}/temp_fp"
|
| 132 |
+
success "Face parsing downloaded"
|
| 133 |
+
fi
|
| 134 |
+
|
| 135 |
+
# Also need resnet18
|
| 136 |
+
if [ ! -f "${MODELS_DIR}/face_parse/resnet18-5c106cde.pth" ]; then
|
| 137 |
+
wget -q -O "${MODELS_DIR}/face_parse/resnet18-5c106cde.pth" \
|
| 138 |
+
"https://download.pytorch.org/models/resnet18-5c106cde.pth" \
|
| 139 |
+
|| curl -s -o "${MODELS_DIR}/face_parse/resnet18-5c106cde.pth" \
|
| 140 |
+
"https://download.pytorch.org/models/resnet18-5c106cde.pth"
|
| 141 |
+
fi
|
| 142 |
+
fi
|
| 143 |
+
|
| 144 |
+
# ============================================================
|
| 145 |
+
# SUMMARY
|
| 146 |
+
# ============================================================
|
| 147 |
+
echo ""
|
| 148 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 149 |
+
echo "║ DOWNLOAD COMPLETE ║"
|
| 150 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 151 |
+
echo ""
|
| 152 |
+
|
| 153 |
+
echo "Models directory: ${MODELS_DIR}"
|
| 154 |
+
echo ""
|
| 155 |
+
|
| 156 |
+
# Check all models
|
| 157 |
+
check_model() {
|
| 158 |
+
if [ -f "$1" ] || [ -d "$1" ]; then
|
| 159 |
+
echo -e " ${GREEN}✓${NC} $2"
|
| 160 |
+
else
|
| 161 |
+
echo -e " ✗ $2 (missing)"
|
| 162 |
+
fi
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
echo "Installed models:"
|
| 166 |
+
check_model "${MODELS_DIR}/avatar/musetalk/pytorch_model.bin" "MuseTalk UNet"
|
| 167 |
+
check_model "${MODELS_DIR}/avatar/vae/config.json" "VAE"
|
| 168 |
+
check_model "${MODELS_DIR}/whisper/tiny.pt" "Whisper Tiny"
|
| 169 |
+
check_model "${MODELS_DIR}/dwpose/dw-ll_ucoco_384.pth" "DWPose"
|
| 170 |
+
check_model "${MODELS_DIR}/face_parse/79999_iter.pth" "Face Parsing"
|
| 171 |
+
echo ""
|
server/fast_engine.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fast Avatar Engine - MuseTalk Integration for real-time lip-sync avatar generation.
|
| 3 |
+
Provides streaming video frame generation from audio input.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import logging
|
| 9 |
+
import asyncio
|
| 10 |
+
import tempfile
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional, Generator, List, Callable
|
| 13 |
+
import numpy as np
|
| 14 |
+
import cv2
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
logging.basicConfig(level=logging.INFO)
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
# Add server directory to path for imports
|
| 21 |
+
SERVER_DIR = Path(__file__).parent
|
| 22 |
+
sys.path.insert(0, str(SERVER_DIR))
|
| 23 |
+
|
| 24 |
+
# Model paths
|
| 25 |
+
MODELS_DIR = Path(os.getenv("MODELS_DIR", SERVER_DIR.parent / "models"))
|
| 26 |
+
AVATAR_DIR = MODELS_DIR / "avatar"
|
| 27 |
+
MUSETALK_DIR = AVATAR_DIR / "musetalk"
|
| 28 |
+
VAE_DIR = AVATAR_DIR / "vae"
|
| 29 |
+
WHISPER_DIR = MODELS_DIR / "whisper"
|
| 30 |
+
DWPOSE_DIR = MODELS_DIR / "dwpose"
|
| 31 |
+
FACE_PARSE_DIR = MODELS_DIR / "face_parse"
|
| 32 |
+
|
| 33 |
+
# Default avatar image
|
| 34 |
+
DEFAULT_AVATAR = os.getenv("AVATAR_IMAGE", str(SERVER_DIR.parent / "assets" / "avatar.png"))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class MuseTalkEngine:
|
| 38 |
+
"""
|
| 39 |
+
MuseTalk-based avatar engine for real-time lip-sync video generation.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def __init__(self,
|
| 43 |
+
avatar_image: Optional[str] = None,
|
| 44 |
+
resolution: int = 256,
|
| 45 |
+
fps: int = 25,
|
| 46 |
+
use_float16: bool = True):
|
| 47 |
+
"""
|
| 48 |
+
Initialize the MuseTalk engine.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
avatar_image: Path to the avatar image
|
| 52 |
+
resolution: Output resolution (default 256x256)
|
| 53 |
+
fps: Frames per second (default 25)
|
| 54 |
+
use_float16: Use FP16 for faster inference
|
| 55 |
+
"""
|
| 56 |
+
self.avatar_image = avatar_image or DEFAULT_AVATAR
|
| 57 |
+
self.resolution = resolution
|
| 58 |
+
self.fps = fps
|
| 59 |
+
self.use_float16 = use_float16
|
| 60 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 61 |
+
|
| 62 |
+
self._avatar_loaded = False
|
| 63 |
+
self._models_loaded = False
|
| 64 |
+
|
| 65 |
+
# Models
|
| 66 |
+
self.vae = None
|
| 67 |
+
self.unet = None
|
| 68 |
+
self.audio2feature = None
|
| 69 |
+
self.face_parser = None
|
| 70 |
+
|
| 71 |
+
# Cached avatar data
|
| 72 |
+
self.avatar_frame = None
|
| 73 |
+
self.avatar_latents = None
|
| 74 |
+
self.face_bbox = None
|
| 75 |
+
self.idle_frames = []
|
| 76 |
+
|
| 77 |
+
logger.info(f"[MuseTalk] Initialized with device: {self.device}")
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def avatar_loaded(self) -> bool:
|
| 81 |
+
"""Check if avatar is loaded and ready."""
|
| 82 |
+
return self._avatar_loaded and self._models_loaded
|
| 83 |
+
|
| 84 |
+
def _check_models(self) -> bool:
|
| 85 |
+
"""Check if all required models exist."""
|
| 86 |
+
required_paths = [
|
| 87 |
+
(VAE_DIR, "VAE model"),
|
| 88 |
+
(MUSETALK_DIR / "musetalk.json", "MuseTalk config"),
|
| 89 |
+
(MUSETALK_DIR / "pytorch_model.bin", "MuseTalk weights"),
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
for path, name in required_paths:
|
| 93 |
+
if not path.exists():
|
| 94 |
+
logger.warning(f"[MuseTalk] Missing {name}: {path}")
|
| 95 |
+
return False
|
| 96 |
+
|
| 97 |
+
return True
|
| 98 |
+
|
| 99 |
+
def load_models(self) -> bool:
|
| 100 |
+
"""Load all required models."""
|
| 101 |
+
if self._models_loaded:
|
| 102 |
+
return True
|
| 103 |
+
|
| 104 |
+
if not self._check_models():
|
| 105 |
+
logger.error("[MuseTalk] Required models not found")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
logger.info("[MuseTalk] Loading models...")
|
| 110 |
+
|
| 111 |
+
# Load VAE
|
| 112 |
+
from musetalk.models.vae import VAE
|
| 113 |
+
self.vae = VAE(
|
| 114 |
+
model_path=str(VAE_DIR),
|
| 115 |
+
resized_img=self.resolution,
|
| 116 |
+
use_float16=self.use_float16
|
| 117 |
+
)
|
| 118 |
+
logger.info("[MuseTalk] VAE loaded")
|
| 119 |
+
|
| 120 |
+
# Load UNet
|
| 121 |
+
from musetalk.models.unet import UNet
|
| 122 |
+
self.unet = UNet(
|
| 123 |
+
unet_config=str(MUSETALK_DIR / "musetalk.json"),
|
| 124 |
+
model_path=str(MUSETALK_DIR / "pytorch_model.bin"),
|
| 125 |
+
use_float16=self.use_float16,
|
| 126 |
+
device=self.device
|
| 127 |
+
)
|
| 128 |
+
logger.info("[MuseTalk] UNet loaded")
|
| 129 |
+
|
| 130 |
+
# Load Audio2Feature (Whisper)
|
| 131 |
+
whisper_path = WHISPER_DIR / "tiny.pt"
|
| 132 |
+
if whisper_path.exists():
|
| 133 |
+
from musetalk.whisper.audio2feature import Audio2Feature
|
| 134 |
+
self.audio2feature = Audio2Feature(
|
| 135 |
+
whisper_model_type="tiny",
|
| 136 |
+
model_path=str(whisper_path)
|
| 137 |
+
)
|
| 138 |
+
logger.info("[MuseTalk] Audio2Feature loaded")
|
| 139 |
+
else:
|
| 140 |
+
logger.warning(f"[MuseTalk] Whisper model not found: {whisper_path}")
|
| 141 |
+
|
| 142 |
+
self._models_loaded = True
|
| 143 |
+
logger.info("[MuseTalk] All models loaded successfully")
|
| 144 |
+
return True
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.error(f"[MuseTalk] Error loading models: {e}")
|
| 148 |
+
import traceback
|
| 149 |
+
traceback.print_exc()
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
def load_avatar(self, avatar_path: Optional[str] = None) -> bool:
|
| 153 |
+
"""
|
| 154 |
+
Load and preprocess the avatar image.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
avatar_path: Path to avatar image (uses default if None)
|
| 158 |
+
"""
|
| 159 |
+
if avatar_path:
|
| 160 |
+
self.avatar_image = avatar_path
|
| 161 |
+
|
| 162 |
+
if not os.path.exists(self.avatar_image):
|
| 163 |
+
logger.error(f"[MuseTalk] Avatar image not found: {self.avatar_image}")
|
| 164 |
+
return False
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
logger.info(f"[MuseTalk] Loading avatar: {self.avatar_image}")
|
| 168 |
+
|
| 169 |
+
# Load and resize avatar
|
| 170 |
+
avatar = cv2.imread(self.avatar_image)
|
| 171 |
+
if avatar is None:
|
| 172 |
+
logger.error(f"[MuseTalk] Failed to read avatar image")
|
| 173 |
+
return False
|
| 174 |
+
|
| 175 |
+
self.avatar_frame = cv2.resize(
|
| 176 |
+
avatar,
|
| 177 |
+
(self.resolution, self.resolution),
|
| 178 |
+
interpolation=cv2.INTER_LANCZOS4
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Precompute avatar latents if models are loaded
|
| 182 |
+
if self._models_loaded and self.vae:
|
| 183 |
+
self.avatar_latents = self.vae.get_latents_for_unet(self.avatar_frame)
|
| 184 |
+
logger.info("[MuseTalk] Avatar latents computed")
|
| 185 |
+
|
| 186 |
+
# Generate idle frames (just the static avatar for now)
|
| 187 |
+
self.idle_frames = [self.avatar_frame.copy() for _ in range(10)]
|
| 188 |
+
|
| 189 |
+
self._avatar_loaded = True
|
| 190 |
+
logger.info("[MuseTalk] Avatar loaded successfully")
|
| 191 |
+
return True
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
logger.error(f"[MuseTalk] Error loading avatar: {e}")
|
| 195 |
+
import traceback
|
| 196 |
+
traceback.print_exc()
|
| 197 |
+
return False
|
| 198 |
+
|
| 199 |
+
def get_idle_frames(self) -> List[np.ndarray]:
|
| 200 |
+
"""Get idle animation frames."""
|
| 201 |
+
if not self.idle_frames and self.avatar_frame is not None:
|
| 202 |
+
return [self.avatar_frame.copy()]
|
| 203 |
+
return self.idle_frames
|
| 204 |
+
|
| 205 |
+
def _process_audio(self, audio_path: str) -> Optional[np.ndarray]:
|
| 206 |
+
"""Extract audio features using Whisper."""
|
| 207 |
+
if not self.audio2feature:
|
| 208 |
+
logger.warning("[MuseTalk] Audio2Feature not loaded")
|
| 209 |
+
return None
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
feature_array = self.audio2feature.audio2feat(audio_path)
|
| 213 |
+
return feature_array
|
| 214 |
+
except Exception as e:
|
| 215 |
+
logger.error(f"[MuseTalk] Error processing audio: {e}")
|
| 216 |
+
return None
|
| 217 |
+
|
| 218 |
+
def generate_frames_streaming(
|
| 219 |
+
self,
|
| 220 |
+
audio_path: str,
|
| 221 |
+
resolution: int = 256,
|
| 222 |
+
batch_size: int = 4
|
| 223 |
+
) -> Generator[np.ndarray, None, None]:
|
| 224 |
+
"""
|
| 225 |
+
Generate video frames from audio in a streaming fashion.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
audio_path: Path to audio file
|
| 229 |
+
resolution: Output resolution
|
| 230 |
+
batch_size: Batch size for inference
|
| 231 |
+
|
| 232 |
+
Yields:
|
| 233 |
+
Video frames as numpy arrays (BGR format)
|
| 234 |
+
"""
|
| 235 |
+
if not self.avatar_loaded:
|
| 236 |
+
logger.error("[MuseTalk] Engine not ready - avatar not loaded")
|
| 237 |
+
# Yield a black frame as fallback
|
| 238 |
+
yield np.zeros((resolution, resolution, 3), dtype=np.uint8)
|
| 239 |
+
return
|
| 240 |
+
|
| 241 |
+
# If models aren't fully loaded, return static avatar
|
| 242 |
+
if not self._models_loaded or not self.audio2feature:
|
| 243 |
+
logger.warning("[MuseTalk] Models not fully loaded, returning static avatar")
|
| 244 |
+
# Estimate frame count from audio duration
|
| 245 |
+
import subprocess
|
| 246 |
+
try:
|
| 247 |
+
result = subprocess.run(
|
| 248 |
+
['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
| 249 |
+
'-of', 'default=noprint_wrappers=1:nokey=1', audio_path],
|
| 250 |
+
capture_output=True, text=True
|
| 251 |
+
)
|
| 252 |
+
duration = float(result.stdout.strip())
|
| 253 |
+
num_frames = int(duration * self.fps)
|
| 254 |
+
except:
|
| 255 |
+
num_frames = 25 # Default 1 second
|
| 256 |
+
|
| 257 |
+
for _ in range(num_frames):
|
| 258 |
+
yield self.avatar_frame.copy()
|
| 259 |
+
return
|
| 260 |
+
|
| 261 |
+
try:
|
| 262 |
+
# Extract audio features
|
| 263 |
+
logger.info(f"[MuseTalk] Processing audio: {audio_path}")
|
| 264 |
+
feature_array = self._process_audio(audio_path)
|
| 265 |
+
|
| 266 |
+
if feature_array is None:
|
| 267 |
+
logger.error("[MuseTalk] Failed to extract audio features")
|
| 268 |
+
yield self.avatar_frame.copy()
|
| 269 |
+
return
|
| 270 |
+
|
| 271 |
+
# Get whisper chunks for each frame
|
| 272 |
+
whisper_chunks = self.audio2feature.feature2chunks(
|
| 273 |
+
feature_array,
|
| 274 |
+
fps=self.fps,
|
| 275 |
+
audio_feat_length=[2, 2]
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
logger.info(f"[MuseTalk] Generating {len(whisper_chunks)} frames")
|
| 279 |
+
|
| 280 |
+
# Generate frames in batches
|
| 281 |
+
for i in range(0, len(whisper_chunks), batch_size):
|
| 282 |
+
batch_chunks = whisper_chunks[i:i + batch_size]
|
| 283 |
+
|
| 284 |
+
for chunk in batch_chunks:
|
| 285 |
+
try:
|
| 286 |
+
# Convert chunk to tensor
|
| 287 |
+
audio_feat = torch.from_numpy(chunk).unsqueeze(0)
|
| 288 |
+
audio_feat = audio_feat.to(self.device)
|
| 289 |
+
|
| 290 |
+
if self.use_float16:
|
| 291 |
+
audio_feat = audio_feat.half()
|
| 292 |
+
|
| 293 |
+
# Add positional encoding
|
| 294 |
+
audio_feat = self.unet.pe(audio_feat)
|
| 295 |
+
|
| 296 |
+
# Generate frame using UNet
|
| 297 |
+
with torch.no_grad():
|
| 298 |
+
latents = self.unet.model(
|
| 299 |
+
self.avatar_latents.to(self.device),
|
| 300 |
+
0, # timestep
|
| 301 |
+
encoder_hidden_states=audio_feat
|
| 302 |
+
).sample
|
| 303 |
+
|
| 304 |
+
# Decode latents to image
|
| 305 |
+
frame = self.vae.decode_latents(latents)[0]
|
| 306 |
+
|
| 307 |
+
# Resize if needed
|
| 308 |
+
if frame.shape[0] != resolution or frame.shape[1] != resolution:
|
| 309 |
+
frame = cv2.resize(frame, (resolution, resolution))
|
| 310 |
+
|
| 311 |
+
yield frame
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
logger.error(f"[MuseTalk] Error generating frame: {e}")
|
| 315 |
+
yield self.avatar_frame.copy()
|
| 316 |
+
|
| 317 |
+
logger.info("[MuseTalk] Frame generation complete")
|
| 318 |
+
|
| 319 |
+
except Exception as e:
|
| 320 |
+
logger.error(f"[MuseTalk] Error in streaming generation: {e}")
|
| 321 |
+
import traceback
|
| 322 |
+
traceback.print_exc()
|
| 323 |
+
yield self.avatar_frame.copy()
|
| 324 |
+
|
| 325 |
+
async def generate_video_fast(
|
| 326 |
+
self,
|
| 327 |
+
audio_path: str,
|
| 328 |
+
output_path: str,
|
| 329 |
+
resolution: int = 256,
|
| 330 |
+
batch_size: int = 8,
|
| 331 |
+
callback: Optional[Callable[[int, int], None]] = None
|
| 332 |
+
) -> bool:
|
| 333 |
+
"""
|
| 334 |
+
Generate a complete video from audio.
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
audio_path: Path to input audio
|
| 338 |
+
output_path: Path for output video
|
| 339 |
+
resolution: Output resolution
|
| 340 |
+
batch_size: Batch size for inference
|
| 341 |
+
callback: Progress callback (current_frame, total_frames)
|
| 342 |
+
|
| 343 |
+
Returns:
|
| 344 |
+
True if successful
|
| 345 |
+
"""
|
| 346 |
+
try:
|
| 347 |
+
frames = []
|
| 348 |
+
frame_count = 0
|
| 349 |
+
|
| 350 |
+
for frame in self.generate_frames_streaming(audio_path, resolution, batch_size):
|
| 351 |
+
frames.append(frame)
|
| 352 |
+
frame_count += 1
|
| 353 |
+
if callback:
|
| 354 |
+
callback(frame_count, -1) # Unknown total
|
| 355 |
+
|
| 356 |
+
if not frames:
|
| 357 |
+
logger.error("[MuseTalk] No frames generated")
|
| 358 |
+
return False
|
| 359 |
+
|
| 360 |
+
# Write video
|
| 361 |
+
temp_video = output_path.replace('.mp4', '_noaudio.mp4')
|
| 362 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
| 363 |
+
out = cv2.VideoWriter(temp_video, fourcc, self.fps, (resolution, resolution))
|
| 364 |
+
|
| 365 |
+
for frame in frames:
|
| 366 |
+
out.write(frame)
|
| 367 |
+
out.release()
|
| 368 |
+
|
| 369 |
+
# Add audio
|
| 370 |
+
import subprocess
|
| 371 |
+
subprocess.run([
|
| 372 |
+
'ffmpeg', '-y',
|
| 373 |
+
'-i', temp_video,
|
| 374 |
+
'-i', audio_path,
|
| 375 |
+
'-c:v', 'libx264',
|
| 376 |
+
'-c:a', 'aac',
|
| 377 |
+
'-shortest',
|
| 378 |
+
output_path
|
| 379 |
+
], check=True, capture_output=True)
|
| 380 |
+
|
| 381 |
+
# Cleanup
|
| 382 |
+
if os.path.exists(temp_video):
|
| 383 |
+
os.remove(temp_video)
|
| 384 |
+
|
| 385 |
+
logger.info(f"[MuseTalk] Video saved: {output_path}")
|
| 386 |
+
return True
|
| 387 |
+
|
| 388 |
+
except Exception as e:
|
| 389 |
+
logger.error(f"[MuseTalk] Error generating video: {e}")
|
| 390 |
+
import traceback
|
| 391 |
+
traceback.print_exc()
|
| 392 |
+
return False
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
# Global engine instance
|
| 396 |
+
_engine: Optional[MuseTalkEngine] = None
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def initialize_engine(
|
| 400 |
+
avatar_path: Optional[str] = None,
|
| 401 |
+
resolution: int = 256,
|
| 402 |
+
fps: int = 25
|
| 403 |
+
) -> MuseTalkEngine:
|
| 404 |
+
"""
|
| 405 |
+
Initialize and return the global MuseTalk engine.
|
| 406 |
+
|
| 407 |
+
Args:
|
| 408 |
+
avatar_path: Path to avatar image
|
| 409 |
+
resolution: Output resolution
|
| 410 |
+
fps: Frames per second
|
| 411 |
+
"""
|
| 412 |
+
global _engine
|
| 413 |
+
|
| 414 |
+
if _engine is not None:
|
| 415 |
+
return _engine
|
| 416 |
+
|
| 417 |
+
_engine = MuseTalkEngine(
|
| 418 |
+
avatar_image=avatar_path,
|
| 419 |
+
resolution=resolution,
|
| 420 |
+
fps=fps
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Try to load models (may fail if not installed)
|
| 424 |
+
_engine.load_models()
|
| 425 |
+
|
| 426 |
+
# Try to load avatar (may fail if image not found)
|
| 427 |
+
_engine.load_avatar()
|
| 428 |
+
|
| 429 |
+
return _engine
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def get_engine() -> Optional[MuseTalkEngine]:
|
| 433 |
+
"""Get the global engine instance."""
|
| 434 |
+
return _engine
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
# Convenience exports
|
| 438 |
+
def generate_video_fast(
|
| 439 |
+
audio_path: str,
|
| 440 |
+
output_path: str,
|
| 441 |
+
resolution: int = 256,
|
| 442 |
+
batch_size: int = 8,
|
| 443 |
+
callback: Optional[Callable[[int, int], None]] = None
|
| 444 |
+
) -> bool:
|
| 445 |
+
"""Generate video from audio using the global engine."""
|
| 446 |
+
engine = get_engine()
|
| 447 |
+
if engine is None:
|
| 448 |
+
logger.error("[MuseTalk] Engine not initialized")
|
| 449 |
+
return False
|
| 450 |
+
|
| 451 |
+
import asyncio
|
| 452 |
+
return asyncio.run(engine.generate_video_fast(
|
| 453 |
+
audio_path, output_path, resolution, batch_size, callback
|
| 454 |
+
))
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def generate_frames_streaming(
|
| 458 |
+
audio_path: str,
|
| 459 |
+
resolution: int = 256,
|
| 460 |
+
batch_size: int = 4
|
| 461 |
+
) -> Generator[np.ndarray, None, None]:
|
| 462 |
+
"""Generate streaming frames from audio using the global engine."""
|
| 463 |
+
engine = get_engine()
|
| 464 |
+
if engine is None:
|
| 465 |
+
logger.error("[MuseTalk] Engine not initialized")
|
| 466 |
+
yield np.zeros((resolution, resolution, 3), dtype=np.uint8)
|
| 467 |
+
return
|
| 468 |
+
|
| 469 |
+
yield from engine.generate_frames_streaming(audio_path, resolution, batch_size)
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
def get_idle_frames() -> List[np.ndarray]:
|
| 473 |
+
"""Get idle animation frames."""
|
| 474 |
+
engine = get_engine()
|
| 475 |
+
if engine is None:
|
| 476 |
+
return [np.zeros((256, 256, 3), dtype=np.uint8)]
|
| 477 |
+
return engine.get_idle_frames()
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
if __name__ == "__main__":
|
| 481 |
+
# Test the engine
|
| 482 |
+
engine = initialize_engine()
|
| 483 |
+
print(f"Avatar loaded: {engine.avatar_loaded}")
|
| 484 |
+
print(f"Models loaded: {engine._models_loaded}")
|
| 485 |
+
|
| 486 |
+
idle = get_idle_frames()
|
| 487 |
+
print(f"Idle frames: {len(idle)}")
|