marcosremar2 Claude Opus 4.5 commited on
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 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)}")