vincentraj commited on
Commit
ec67cd4
·
1 Parent(s): 3b25f52

Initial Commit

Browse files
handler.py CHANGED
@@ -1,470 +1,559 @@
1
- import torch
2
  import os
3
- import shutil
4
  import subprocess
 
5
  import json
6
  import tempfile
 
 
 
 
7
  import base64
8
- from typing import Dict, Any, Optional, List
9
- import soundfile as sf
10
  from datetime import datetime
11
- from huggingface_hub import snapshot_download
12
-
13
-
14
- class EndpointHandler:
15
- def __init__(self, path=""):
16
- """
17
- Initialize the handler for Meigen MultiTalk inference.
18
- Optimized for serverless deployment with pre-downloaded models.
19
-
20
- Args:
21
- path: Path to the model directory (not used in this implementation)
22
- """
23
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
24
 
25
- # Set up the script path - try multiple possible locations
26
- self.script_path = self.find_script_path()
27
- if not self.script_path:
28
- raise RuntimeError("Could not locate generate_multitalk.py script")
29
 
30
- print(f"Found script at: {self.script_path}")
31
-
32
- self.setup_models()
33
- self.setup_gpu_config()
34
-
35
- def find_script_path(self) -> Optional[str]:
36
- """
37
- Find the location of the generate_multitalk.py script.
38
-
39
- Returns:
40
- Path to the script if found, None otherwise
41
- """
42
- possible_paths = [
43
- "generate_multitalk.py", # Current directory
44
- "/app/generate_multitalk.py", # App directory
45
- "./src/generate_multitalk.py", # Src directory
46
- "../generate_multitalk.py", # Parent directory
47
- "./weights/MeiGen-MultiTalk/generate_multitalk.py", # In model directory
48
- "./MeiGen-MultiTalk/generate_multitalk.py", # Alternative model directory
49
- ]
50
-
51
- for path in possible_paths:
52
- if os.path.exists(path):
53
- return os.path.abspath(path)
54
-
55
- # Try to find it recursively in common directories
56
- search_dirs = ["/app", ".", "./weights", "./src"]
57
- for search_dir in search_dirs:
58
- if os.path.exists(search_dir):
59
- for root, dirs, files in os.walk(search_dir):
60
- if "generate_multitalk.py" in files:
61
- return os.path.join(root, "generate_multitalk.py")
62
-
63
- return None
64
-
65
- def setup_models(self):
66
- """Setup models - download only if not already present."""
67
- print("Setting up models...")
68
-
69
- # Define model paths
70
- self.wan_model_path = "./weights/Wan2.1-I2V-14B-480P"
71
- self.wav2vec_path = "./weights/chinese-wav2vec2-base"
72
- self.multitalk_path = "./weights/MeiGen-MultiTalk"
73
-
74
- # Only download if models don't exist (for local development)
75
- # In serverless deployment, these should be pre-baked into the container
76
- if not os.path.exists(self.wan_model_path):
77
- print("Downloading Wan2.1-I2V-14B-480P model...")
78
- self.wan_model_path = snapshot_download(
79
- repo_id="Wan-AI/Wan2.1-I2V-14B-480P",
80
- local_dir=self.wan_model_path,
81
- )
82
-
83
- if not os.path.exists(self.wav2vec_path):
84
- print("Downloading Chinese wav2vec2 model...")
85
- self.wav2vec_path = snapshot_download(
86
- repo_id="TencentGameMate/chinese-wav2vec2-base",
87
- local_dir=self.wav2vec_path,
88
- )
89
-
90
- if not os.path.exists(self.multitalk_path):
91
- print("Downloading MeiGen MultiTalk weights...")
92
- self.multitalk_path = snapshot_download(
93
- repo_id="MeiGen-AI/MeiGen-MultiTalk",
94
- local_dir=self.multitalk_path,
95
- )
96
-
97
- # Setup model files if not already done
98
- self.setup_model_files()
99
-
100
- def setup_model_files(self):
101
- """Setup model files by copying and renaming as needed."""
102
- base_model_dir = self.wan_model_path
103
- multitalk_dir = self.multitalk_path
104
-
105
- # Check if setup is already done
106
- multitalk_weights_path = os.path.join(base_model_dir, "multitalk.safetensors")
107
- if os.path.exists(multitalk_weights_path):
108
- print("Model files already set up, skipping setup.")
109
- return
110
-
111
- # File to rename
112
- original_index = os.path.join(
113
- base_model_dir, "diffusion_pytorch_model.safetensors.index.json"
114
  )
115
- backup_index = os.path.join(
116
- base_model_dir, "diffusion_pytorch_model.safetensors.index.json_old"
 
 
 
117
  )
118
-
119
- # Rename the original index file
120
- if os.path.exists(original_index) and not os.path.exists(backup_index):
121
- os.rename(original_index, backup_index)
122
- print("Renamed original index file to .json_old")
123
-
124
- # Copy updated index file from MultiTalk
125
- multitalk_index = os.path.join(
126
- multitalk_dir, "diffusion_pytorch_model.safetensors.index.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  )
128
- if os.path.exists(multitalk_index):
129
- shutil.copy2(multitalk_index, base_model_dir)
130
-
131
- # Copy MultiTalk model weights
132
- multitalk_weights_src = os.path.join(multitalk_dir, "multitalk.safetensors")
133
- if os.path.exists(multitalk_weights_src):
134
- shutil.copy2(multitalk_weights_src, base_model_dir)
135
-
136
- print("Copied MultiTalk files into base model directory.")
137
-
138
- def setup_gpu_config(self):
139
- """Setup GPU configuration and VRAM parameters."""
140
- self.num_gpus = torch.cuda.device_count()
141
- print(f"GPU AVAILABLE: {self.num_gpus}")
142
-
143
- if torch.cuda.is_available():
144
- self.gpu_name = torch.cuda.get_device_name(torch.cuda.current_device())
145
- print(f"Current GPU: {self.gpu_name}")
146
-
147
- # GPU to VRAM mapping
148
- GPU_TO_VRAM_PARAMS = {
149
- "NVIDIA A100": 11000000000,
150
- "NVIDIA A100-SXM4-40GB": 11000000000,
151
- "NVIDIA A100-SXM4-80GB": 22000000000,
152
- "NVIDIA L4": 5000000000,
153
- "NVIDIA L40S": 11000000000,
154
- }
155
-
156
- self.used_vram_params = GPU_TO_VRAM_PARAMS.get(self.gpu_name, 5000000000)
157
- print(f"Using {self.used_vram_params} for num_persistent_param_in_dit")
158
- else:
159
- raise RuntimeError("No CUDA-compatible GPU found. A GPU is required.")
160
 
161
- def trim_audio_to_5s_temp(self, audio_path: str, sample_rate: int = 16000) -> str:
162
- """
163
- Trim audio to 5 seconds and save to temporary file.
164
-
165
- Args:
166
- audio_path: Path to input audio file
167
- sample_rate: Target sample rate
168
 
169
- Returns:
170
- Path to trimmed audio file
171
- """
172
- max_duration_sec = 5
173
- audio, sr = sf.read(audio_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- if sr != sample_rate:
176
- sample_rate = sr
177
 
178
- max_samples = max_duration_sec * sample_rate
179
- if len(audio) > max_samples:
180
- audio = audio[:max_samples]
 
 
 
181
 
182
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
183
- base_name = os.path.splitext(os.path.basename(audio_path))[0]
184
- temp_filename = f"{base_name}_trimmed_{timestamp}.wav"
185
- temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
186
 
187
- sf.write(temp_path, audio, samplerate=sample_rate)
188
- return temp_path
 
189
 
190
- def save_base64_file(self, base64_data: str, file_extension: str) -> str:
191
- """
192
- Save base64 encoded file to temporary location.
193
 
194
- Args:
195
- base64_data: Base64 encoded file data
196
- file_extension: File extension (e.g., '.png', '.wav')
 
 
 
 
197
 
198
- Returns:
199
- Path to saved file
200
- """
201
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
202
- temp_filename = f"temp_file_{timestamp}{file_extension}"
203
- temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
204
-
205
- with open(temp_path, "wb") as f:
206
- f.write(base64.b64decode(base64_data))
207
-
208
- return temp_path
209
-
210
- def create_temp_input_json(
211
- self,
212
- prompt: str,
213
- cond_image_path: str,
214
- cond_audio_path_spk1: str,
215
- cond_audio_path_spk2: Optional[str] = None,
216
- ) -> str:
217
- """
218
- Create a temporary JSON file with the user-provided prompt, image, and audio paths.
219
 
220
- Args:
221
- prompt: Text prompt for generation
222
- cond_image_path: Path to conditioning image
223
- cond_audio_path_spk1: Path to first speaker audio
224
- cond_audio_path_spk2: Path to second speaker audio (optional)
 
 
 
225
 
226
- Returns:
227
- Path to temporary JSON file
228
- """
229
- if cond_audio_path_spk2 is None:
230
- data = {
231
- "prompt": prompt,
232
- "cond_image": cond_image_path,
233
- "cond_audio": {"person1": cond_audio_path_spk1},
234
- }
235
- else:
236
- data = {
237
- "prompt": prompt,
238
- "cond_image": cond_image_path,
239
- "audio_type": "para",
240
- "cond_audio": {
241
- "person1": cond_audio_path_spk1,
242
- "person2": cond_audio_path_spk2,
243
- },
244
- }
245
 
246
- temp_json = tempfile.NamedTemporaryFile(
247
- delete=False, suffix=".json", mode="w", encoding="utf-8"
 
 
 
 
 
 
 
 
 
 
248
  )
249
- json.dump(data, temp_json, indent=4)
250
- temp_json_path = temp_json.name
251
- temp_json.close()
252
-
253
- print(f"Temporary input JSON saved to: {temp_json_path}")
254
- return temp_json_path
255
-
256
- def run_inference(
257
- self,
258
- prompt: str,
259
- cond_image_path: str,
260
- cond_audio_path_spk1: str,
261
- cond_audio_path_spk2: Optional[str] = None,
262
- sample_steps: int = 12,
263
- ) -> str:
264
- """
265
- Run the MultiTalk inference.
266
 
267
- Args:
268
- prompt: Text prompt
269
- cond_image_path: Path to conditioning image
270
- cond_audio_path_spk1: Path to first speaker audio
271
- cond_audio_path_spk2: Path to second speaker audio (optional)
272
- sample_steps: Number of sampling steps
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
- Returns:
275
- Path to generated video
276
- """
277
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
278
- result_filename = f"meigen_multitalk_result_{sample_steps}_steps_{timestamp}"
279
- temp_files_to_cleanup = []
280
 
281
- try:
282
- # Trim audio files to 5 seconds
283
- trimmed_audio_path_spk1 = self.trim_audio_to_5s_temp(cond_audio_path_spk1)
284
- if trimmed_audio_path_spk1 != cond_audio_path_spk1:
285
- cond_audio_path_spk1 = trimmed_audio_path_spk1
286
- temp_files_to_cleanup.append(trimmed_audio_path_spk1)
287
-
288
- if cond_audio_path_spk2 is not None:
289
- trimmed_audio_path_spk2 = self.trim_audio_to_5s_temp(
290
- cond_audio_path_spk2
291
  )
292
- if trimmed_audio_path_spk2 != cond_audio_path_spk2:
293
- cond_audio_path_spk2 = trimmed_audio_path_spk2
294
- temp_files_to_cleanup.append(trimmed_audio_path_spk2)
295
-
296
- # Prepare input JSON
297
- input_json_path = self.create_temp_input_json(
298
- prompt, cond_image_path, cond_audio_path_spk1, cond_audio_path_spk2
299
- )
300
- temp_files_to_cleanup.append(input_json_path)
301
-
302
- # Base command arguments
303
- common_args = [
304
- "--ckpt_dir",
305
- self.wan_model_path,
306
- "--wav2vec_dir",
307
- self.wav2vec_path,
308
- "--input_json",
309
- input_json_path,
310
- "--sample_steps",
311
- str(sample_steps),
312
- "--mode",
313
- "streaming",
314
- "--use_teacache",
315
- "--save_file",
316
- result_filename,
317
- ]
318
-
319
- # Get the directory containing the script to set as working directory
320
- script_dir = os.path.dirname(self.script_path)
321
- script_name = os.path.basename(self.script_path)
322
-
323
- # Choose command based on number of GPUs
324
- if self.num_gpus > 1:
325
- cmd = [
326
- "torchrun",
327
- f"--nproc_per_node={self.num_gpus}",
328
- "--standalone",
329
- script_name, # Use just the script name since we'll set cwd
330
- "--dit_fsdp",
331
- "--t5_fsdp",
332
- "--ulysses_size",
333
- str(self.num_gpus),
334
- ] + common_args
335
- else:
336
- cmd = [
337
- "python3",
338
- script_name, # Use just the script name since we'll set cwd
339
- "--num_persistent_param_in_dit",
340
- str(self.used_vram_params),
341
- ] + common_args
342
-
343
- print(f"Running command: {' '.join(cmd)}")
344
- print(f"Working directory: {script_dir}")
345
-
346
- # Run inference with correct working directory
347
- with open("inference.log", "w") as log_file:
348
- process = subprocess.Popen(
349
- cmd,
350
- cwd=script_dir, # Set working directory to script location
351
- stdout=subprocess.PIPE,
352
- stderr=subprocess.STDOUT,
353
- text=True,
354
- bufsize=1,
355
  )
356
- for line in process.stdout:
357
- print(line, end="")
358
- log_file.write(line)
359
- process.wait()
360
-
361
- if process.returncode != 0:
362
- raise RuntimeError("Inference failed. Check inference.log for details.")
363
-
364
- # The output video might be in the script directory
365
- output_video_path = os.path.join(script_dir, f"{result_filename}.mp4")
366
- if not os.path.exists(output_video_path):
367
- # Try current directory as fallback
368
- output_video_path = f"{result_filename}.mp4"
369
-
370
- if not os.path.exists(output_video_path):
371
- raise RuntimeError(f"Output video not found: {output_video_path}")
372
-
373
- return output_video_path
374
-
375
- finally:
376
- # Clean up temporary files
377
- for f in temp_files_to_cleanup:
378
- try:
379
- if os.path.exists(f):
380
- os.remove(f)
381
- print(f"[INFO] Removed temporary file: {f}")
382
- except Exception as e:
383
- print(f"[WARNING] Could not remove {f}: {e}")
384
 
385
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
386
  """
387
- Main inference method called by Hugging Face Inference API.
388
-
389
- Args:
390
- data: Input data dictionary containing:
391
- - inputs: Main input data
392
- - parameters: Optional parameters
393
-
394
- Returns:
395
- Dictionary containing the generated video
 
 
 
 
 
 
 
 
 
396
  """
397
  try:
398
- # Extract inputs
399
  inputs = data.get("inputs", {})
400
  parameters = data.get("parameters", {})
401
 
402
- # Required inputs
403
- prompt = inputs.get("prompt", "")
404
- if not prompt:
405
- raise ValueError("Prompt is required")
406
-
407
- # Image input (base64 encoded)
408
- image_data = inputs.get("image")
409
- if not image_data:
410
- raise ValueError("Image is required")
411
-
412
- # Audio inputs (base64 encoded)
413
- audio_spk1_data = inputs.get("audio_spk1")
414
- if not audio_spk1_data:
415
- raise ValueError("Audio for speaker 1 is required")
416
-
417
- audio_spk2_data = inputs.get("audio_spk2") # Optional
418
-
419
- # Parameters
420
- sample_steps = parameters.get("sample_steps", 12)
421
- sample_steps = max(2, min(25, sample_steps)) # Clamp between 2 and 25
422
-
423
- # Save base64 files to temporary locations
424
- temp_files_to_cleanup = []
425
-
426
- # Save image
427
- image_path = self.save_base64_file(image_data, ".png")
428
- temp_files_to_cleanup.append(image_path)
429
-
430
- # Save audio files
431
- audio_spk1_path = self.save_base64_file(audio_spk1_data, ".wav")
432
- temp_files_to_cleanup.append(audio_spk1_path)
433
-
434
- audio_spk2_path = None
435
- if audio_spk2_data:
436
- audio_spk2_path = self.save_base64_file(audio_spk2_data, ".wav")
437
- temp_files_to_cleanup.append(audio_spk2_path)
438
-
439
- # Run inference
440
- output_video_path = self.run_inference(
441
- prompt=prompt,
442
- cond_image_path=image_path,
443
- cond_audio_path_spk1=audio_spk1_path,
444
- cond_audio_path_spk2=audio_spk2_path,
445
- sample_steps=sample_steps,
446
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
- # Read and encode output video
449
- with open(output_video_path, "rb") as f:
450
- video_data = base64.b64encode(f.read()).decode()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
 
452
- # Clean up input files
453
- for f in temp_files_to_cleanup:
454
- try:
455
- if os.path.exists(f):
456
- os.remove(f)
457
- except Exception as e:
458
- print(f"[WARNING] Could not remove {f}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
 
460
- # Clean up output video
461
- try:
462
- if os.path.exists(output_video_path):
463
- os.remove(output_video_path)
464
- except Exception as e:
465
- print(f"[WARNING] Could not remove output video: {e}")
 
 
 
 
 
 
 
 
 
466
 
467
- return {"generated_video": video_data, "content_type": "video/mp4"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
  except Exception as e:
470
- return {"error": str(e)}
 
 
1
+ # Hugging Face Inference Endpoint Handler for MultiTalk
2
  import os
 
3
  import subprocess
4
+ import time
5
  import json
6
  import tempfile
7
+ import logging
8
+ import sys
9
+ import warnings
10
+ import shutil
11
  import base64
12
+ from typing import Optional, Dict, Any, Union
 
13
  from datetime import datetime
14
+ from types import SimpleNamespace
15
+ from io import BytesIO
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # Suppress warnings for cleaner output
18
+ warnings.filterwarnings("ignore")
 
 
19
 
20
+ import torch
21
+ import numpy as np
22
+ import random
23
+ import soundfile as sf
24
+ from PIL import Image
25
+
26
+ # Import MultiTalk components
27
+ import wan
28
+ from wan.configs import WAN_CONFIGS
29
+ from transformers import Wav2Vec2FeatureExtractor
30
+ from src.audio_analysis.wav2vec2 import Wav2Vec2Model
31
+ import librosa
32
+ import pyloudnorm as pyln
33
+ from einops import rearrange
34
+ from wan.utils.multitalk_utils import save_video_ffmpeg
35
+
36
+
37
+ def loudness_norm(audio_array, sr=16000, lufs=-23):
38
+ meter = pyln.Meter(sr)
39
+ loudness = meter.integrated_loudness(audio_array)
40
+ if abs(loudness) > 100:
41
+ return audio_array
42
+ normalized_audio = pyln.normalize.loudness(audio_array, loudness, lufs)
43
+ return normalized_audio
44
+
45
+
46
+ def extract_audio_from_video(filename, sample_rate=16000):
47
+ """Extract audio from video file with robust error handling"""
48
+ raw_audio_path = f"{os.path.splitext(os.path.basename(filename))[0]}.wav"
49
+ ffmpeg_command = [
50
+ "ffmpeg",
51
+ "-y",
52
+ "-i",
53
+ str(filename),
54
+ "-vn",
55
+ "-acodec",
56
+ "pcm_s16le",
57
+ "-ar",
58
+ str(sample_rate),
59
+ "-ac",
60
+ "2",
61
+ raw_audio_path,
62
+ ]
63
+ subprocess.run(ffmpeg_command, check=True, capture_output=True)
64
+ human_speech_array, sr = librosa.load(raw_audio_path, sr=sample_rate)
65
+ human_speech_array = loudness_norm(human_speech_array, sr)
66
+ os.remove(raw_audio_path)
67
+ return human_speech_array
68
+
69
+
70
+ def audio_prepare_single(audio_path, sample_rate=16000):
71
+ ext = os.path.splitext(audio_path)[1].lower()
72
+ if ext in [".mp4", ".mov", ".avi", ".mkv"]:
73
+ human_speech_array = extract_audio_from_video(audio_path, sample_rate)
74
+ return human_speech_array
75
+ else:
76
+ human_speech_array, sr = librosa.load(audio_path, sr=sample_rate)
77
+ human_speech_array = loudness_norm(human_speech_array, sr)
78
+ return human_speech_array
79
+
80
+
81
+ def audio_prepare_multi(left_path, right_path, audio_type, sample_rate=16000):
82
+ human_speech_array1 = audio_prepare_single(left_path)
83
+ human_speech_array2 = audio_prepare_single(right_path)
84
+
85
+ if audio_type == "para":
86
+ new_human_speech1 = human_speech_array1
87
+ new_human_speech2 = human_speech_array2
88
+ elif audio_type == "add":
89
+ new_human_speech1 = np.concatenate(
90
+ [
91
+ human_speech_array1[: human_speech_array1.shape[0]],
92
+ np.zeros(human_speech_array2.shape[0]),
93
+ ]
 
 
 
 
 
 
 
 
 
 
94
  )
95
+ new_human_speech2 = np.concatenate(
96
+ [
97
+ np.zeros(human_speech_array1.shape[0]),
98
+ human_speech_array2[: human_speech_array2.shape[0]],
99
+ ]
100
  )
101
+ sum_human_speechs = new_human_speech1 + new_human_speech2
102
+ return new_human_speech1, new_human_speech2, sum_human_speechs
103
+
104
+
105
+ def get_embedding(
106
+ speech_array, wav2vec_feature_extractor, audio_encoder, sr=16000, device="cpu"
107
+ ):
108
+ """Extract audio embeddings optimized for GPU processing"""
109
+ audio_duration = len(speech_array) / sr
110
+ video_length = audio_duration * 25 # Assume the video fps is 25
111
+
112
+ # Extract audio features
113
+ audio_feature = np.squeeze(
114
+ wav2vec_feature_extractor(speech_array, sampling_rate=sr).input_values
115
+ )
116
+ audio_feature = torch.from_numpy(audio_feature).float().to(device=device)
117
+ audio_feature = audio_feature.unsqueeze(0)
118
+
119
+ # Generate embeddings on appropriate device
120
+ with torch.no_grad():
121
+ embeddings = audio_encoder(
122
+ audio_feature, seq_len=int(video_length), output_hidden_states=True
123
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
+ if len(embeddings) == 0:
126
+ print("⚠️ Failed to extract audio embedding")
127
+ return None
 
 
 
 
128
 
129
+ audio_emb = torch.stack(embeddings.hidden_states[1:], dim=1).squeeze(0)
130
+ audio_emb = rearrange(audio_emb, "b s d -> s b d")
131
+
132
+ # Keep on CPU for compatibility with downstream processing
133
+ audio_emb = audio_emb.cpu().detach()
134
+ return audio_emb
135
+
136
+
137
+ def download_weights(url: str, dest: str) -> None:
138
+ start = time.time()
139
+ print("[!] Initiating download from URL: ", url)
140
+ print("[~] Destination path: ", dest)
141
+ if ".tar" in dest:
142
+ dest = os.path.dirname(dest)
143
+ command = ["pget", "-vf" + ("x" if ".tar" in url else ""), url, dest]
144
+ try:
145
+ print(f"[~] Running command: {' '.join(command)}")
146
+ subprocess.check_call(command, close_fds=False)
147
+ except subprocess.CalledProcessError as e:
148
+ print(
149
+ f"[ERROR] Failed to download weights. Command '{' '.join(e.cmd)}' returned non-zero exit status {e.returncode}."
150
+ )
151
+ raise
152
+ print("[+] Download completed in: ", time.time() - start, "seconds")
153
 
 
 
154
 
155
+ def decode_base64_file(base64_string: str, output_path: str) -> str:
156
+ """Decode base64 string and save to file"""
157
+ try:
158
+ # Remove data URL prefix if present
159
+ if "," in base64_string:
160
+ base64_string = base64_string.split(",")[1]
161
 
162
+ # Decode and save
163
+ with open(output_path, "wb") as f:
164
+ f.write(base64.b64decode(base64_string))
 
165
 
166
+ return output_path
167
+ except Exception as e:
168
+ raise ValueError(f"Failed to decode base64 file: {str(e)}")
169
 
 
 
 
170
 
171
+ def encode_file_to_base64(file_path: str) -> str:
172
+ """Encode file to base64 string"""
173
+ try:
174
+ with open(file_path, "rb") as f:
175
+ return base64.b64encode(f.read()).decode("utf-8")
176
+ except Exception as e:
177
+ raise ValueError(f"Failed to encode file to base64: {str(e)}")
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ class EndpointHandler:
181
+ def __init__(self, path=""):
182
+ """Initialize the handler"""
183
+ # Model configuration
184
+ self.MODEL_CACHE = "weights"
185
+ self.BASE_URL = (
186
+ f"https://weights.replicate.delivery/default/multitalk/{self.MODEL_CACHE}/"
187
+ )
188
 
189
+ # Set environment variables
190
+ os.environ["HF_HOME"] = self.MODEL_CACHE
191
+ os.environ["TORCH_HOME"] = self.MODEL_CACHE
192
+ os.environ["HF_DATASETS_CACHE"] = self.MODEL_CACHE
193
+ os.environ["TRANSFORMERS_CACHE"] = self.MODEL_CACHE
194
+ os.environ["HUGGINGFACE_HUB_CACHE"] = self.MODEL_CACHE
195
+
196
+ # Create model cache directory if it doesn't exist
197
+ os.makedirs(self.MODEL_CACHE, exist_ok=True)
198
+
199
+ # Download model weights if needed
200
+ model_files = [
201
+ "MeiGen-MultiTalk.tar",
202
+ "Wan2.1-I2V-14B-480P.tar",
203
+ "chinese-wav2vec2-base.tar",
204
+ ]
 
 
 
205
 
206
+ for model_file in model_files:
207
+ url = self.BASE_URL + model_file
208
+ filename = url.split("/")[-1]
209
+ dest_path = os.path.join(self.MODEL_CACHE, filename)
210
+ if not os.path.exists(dest_path.replace(".tar", "")):
211
+ download_weights(url, dest_path)
212
+
213
+ # Set up logging
214
+ logging.basicConfig(
215
+ level=logging.INFO,
216
+ format="[%(asctime)s] %(levelname)s: %(message)s",
217
+ handlers=[logging.StreamHandler(stream=sys.stdout)],
218
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
+ # Model paths
221
+ self.ckpt_dir = "weights/Wan2.1-I2V-14B-480P"
222
+ self.wav2vec_dir = "weights/chinese-wav2vec2-base"
223
+ self.multitalk_dir = "weights/MeiGen-MultiTalk"
224
+
225
+ # Initialize device for single GPU
226
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
227
+ print(f"Using device: {self.device}")
228
+
229
+ # Load wav2vec models optimized for high VRAM
230
+ print("Loading wav2vec models...")
231
+ audio_device = (
232
+ self.device
233
+ if torch.cuda.is_available()
234
+ and torch.cuda.get_device_properties(0).total_memory > 40 * 1024**3
235
+ else "cpu"
236
+ )
237
+ print(f"Loading audio encoder on: {audio_device}")
238
+
239
+ self.audio_encoder = Wav2Vec2Model.from_pretrained(
240
+ self.wav2vec_dir, local_files_only=True, attn_implementation="eager"
241
+ ).to(audio_device)
242
+ self.audio_encoder.feature_extractor._freeze_parameters()
243
+ self.wav2vec_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(
244
+ self.wav2vec_dir, local_files_only=True
245
+ )
246
+ self.audio_device = audio_device
247
+
248
+ # Load MultiTalk pipeline
249
+ print("Loading MultiTalk pipeline...")
250
+ self.cfg = WAN_CONFIGS["multitalk-14B"]
251
+ self.wan_i2v = wan.MultiTalkPipeline(
252
+ config=self.cfg,
253
+ checkpoint_dir=self.ckpt_dir,
254
+ device_id=0,
255
+ rank=0,
256
+ t5_fsdp=False,
257
+ dit_fsdp=False,
258
+ use_usp=False,
259
+ t5_cpu=False, # Keep T5 on GPU for speed
260
+ )
261
 
262
+ # GPU optimizations for high-VRAM setup (A100/H100/H200)
263
+ if torch.cuda.is_available():
264
+ vram_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
265
+ print(f"🔍 Detected {vram_gb:.1f}GB VRAM")
 
 
266
 
267
+ if vram_gb > 40: # High VRAM setup
268
+ print(
269
+ "🚀 High-VRAM detected: Enabling maximum performance optimizations"
 
 
 
 
 
 
 
270
  )
271
+ # Enable advanced GPU features for maximum speed
272
+ torch.backends.cuda.enable_flash_sdp(True)
273
+ torch.backends.cudnn.benchmark = (
274
+ True # Optimize for consistent input sizes
275
+ )
276
+ torch.backends.cuda.matmul.allow_tf32 = (
277
+ True # Enable TF32 for faster matmul
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  )
279
+ torch.backends.cudnn.allow_tf32 = True # Enable TF32 for convolutions
280
+ torch.cuda.empty_cache() # Clear any existing memory
281
+ print(
282
+ "⚡ Enabled Flash-SDP, TF32, and cuDNN benchmarking for maximum throughput"
283
+ )
284
+ else:
285
+ print("🔧 Standard GPU optimizations enabled")
286
+ torch.backends.cuda.enable_flash_sdp(True)
287
+ torch.cuda.empty_cache()
288
+
289
+ print("✅ Model setup completed successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
292
  """
293
+ Handle inference requests
294
+
295
+ Expected input format:
296
+ {
297
+ "inputs": {
298
+ "image": "base64_encoded_image_string",
299
+ "first_audio": "base64_encoded_audio_string",
300
+ "prompt": "Text prompt describing the desired interaction",
301
+ "second_audio": "base64_encoded_audio_string (optional)",
302
+ "num_frames": 81,
303
+ "sampling_steps": 40,
304
+ "seed": 42,
305
+ "turbo": true
306
+ },
307
+ "parameters": {
308
+ // Optional parameters can also be passed here
309
+ }
310
+ }
311
  """
312
  try:
313
+ # Parse input data
314
  inputs = data.get("inputs", {})
315
  parameters = data.get("parameters", {})
316
 
317
+ # Merge parameters with inputs (parameters take precedence)
318
+ config = {**inputs, **parameters}
319
+
320
+ # Validate required inputs
321
+ if "image" not in config:
322
+ raise ValueError("Missing required input: image")
323
+ if "first_audio" not in config:
324
+ raise ValueError("Missing required input: first_audio")
325
+
326
+ # Extract parameters with defaults
327
+ image_b64 = config["image"]
328
+ first_audio_b64 = config["first_audio"]
329
+ prompt = config.get(
330
+ "prompt",
331
+ "A smiling man and woman wearing headphones sit in front of microphones, appearing to host a podcast.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  )
333
+ second_audio_b64 = config.get("second_audio", None)
334
+ num_frames = config.get("num_frames", 81)
335
+ sampling_steps = config.get("sampling_steps", 40)
336
+ seed = config.get("seed", None)
337
+ turbo = config.get("turbo", True)
338
+
339
+ # Validate parameters
340
+ if not isinstance(num_frames, int) or num_frames < 25 or num_frames > 201:
341
+ raise ValueError("num_frames must be an integer between 25 and 201")
342
+ if (
343
+ not isinstance(sampling_steps, int)
344
+ or sampling_steps < 2
345
+ or sampling_steps > 100
346
+ ):
347
+ raise ValueError("sampling_steps must be an integer between 2 and 100")
348
+
349
+ # Auto-correct frame count to nearest valid value (4n+1 format)
350
+ original_frames = num_frames
351
+ if (num_frames - 1) % 4 != 0:
352
+ # Find the nearest valid values
353
+ n_lower = (num_frames - 1) // 4
354
+ n_upper = n_lower + 1
355
+
356
+ frames_lower = 4 * n_lower + 1
357
+ frames_upper = 4 * n_upper + 1
358
+
359
+ # Choose the closer one
360
+ if abs(num_frames - frames_lower) <= abs(num_frames - frames_upper):
361
+ num_frames = frames_lower
362
+ else:
363
+ num_frames = frames_upper
364
+
365
+ # Ensure it's within bounds [25, 201]
366
+ num_frames = max(25, min(num_frames, 201))
367
+
368
+ # Final safety check and adjustment if needed
369
+ while (num_frames - 1) % 4 != 0 and num_frames <= 201:
370
+ num_frames += 1
371
+
372
+ print(
373
+ f"📐 Auto-corrected num_frames from {original_frames} to {num_frames} (required format: 4n+1)"
374
+ )
375
 
376
+ # Set random seed
377
+ if seed is None:
378
+ seed = random.randint(0, 99999999)
379
+
380
+ print(f"🎬 Generating video with seed: {seed}")
381
+
382
+ with tempfile.TemporaryDirectory() as temp_dir:
383
+ # Save input files from base64
384
+ image_path = os.path.join(temp_dir, "input_image.jpg")
385
+ first_audio_path = os.path.join(temp_dir, "first_audio.wav")
386
+
387
+ decode_base64_file(image_b64, image_path)
388
+ decode_base64_file(first_audio_b64, first_audio_path)
389
+
390
+ second_audio_path = None
391
+ if second_audio_b64:
392
+ second_audio_path = os.path.join(temp_dir, "second_audio.wav")
393
+ decode_base64_file(second_audio_b64, second_audio_path)
394
+
395
+ is_multi_person = second_audio_path is not None
396
+ audio_save_dir = os.path.join(temp_dir, "audio_embeddings")
397
+ os.makedirs(audio_save_dir, exist_ok=True)
398
+
399
+ # Process audio and generate embeddings
400
+ if is_multi_person:
401
+ print("🎤 Processing multi-person audio...")
402
+ audio_type = "add" # Sequential by default
403
+ speech1, speech2, combined_speech = audio_prepare_multi(
404
+ first_audio_path, second_audio_path, audio_type
405
+ )
406
+
407
+ # Generate embeddings on optimal device
408
+ embedding1 = get_embedding(
409
+ speech1,
410
+ self.wav2vec_feature_extractor,
411
+ self.audio_encoder,
412
+ device=self.audio_device,
413
+ )
414
+ embedding2 = get_embedding(
415
+ speech2,
416
+ self.wav2vec_feature_extractor,
417
+ self.audio_encoder,
418
+ device=self.audio_device,
419
+ )
420
+
421
+ # Save embeddings and audio
422
+ emb1_path = os.path.join(audio_save_dir, "1.pt")
423
+ emb2_path = os.path.join(audio_save_dir, "2.pt")
424
+ sum_audio_path = os.path.join(audio_save_dir, "sum.wav")
425
+
426
+ torch.save(embedding1, emb1_path)
427
+ torch.save(embedding2, emb2_path)
428
+ sf.write(sum_audio_path, combined_speech, 16000)
429
+
430
+ # Create input data
431
+ input_data = {
432
+ "prompt": prompt,
433
+ "cond_image": image_path,
434
+ "audio_type": audio_type,
435
+ "cond_audio": {"person1": emb1_path, "person2": emb2_path},
436
+ "video_audio": sum_audio_path,
437
+ }
438
+ else:
439
+ print("🎤 Processing single-person audio...")
440
+ speech = audio_prepare_single(first_audio_path)
441
+ embedding = get_embedding(
442
+ speech,
443
+ self.wav2vec_feature_extractor,
444
+ self.audio_encoder,
445
+ device=self.audio_device,
446
+ )
447
+
448
+ # Save embedding and audio
449
+ emb_path = os.path.join(audio_save_dir, "1.pt")
450
+ sum_audio_path = os.path.join(audio_save_dir, "sum.wav")
451
+
452
+ torch.save(embedding, emb_path)
453
+ sf.write(sum_audio_path, speech, 16000)
454
+
455
+ # Create input data
456
+ input_data = {
457
+ "prompt": prompt,
458
+ "cond_image": image_path,
459
+ "cond_audio": {"person1": emb_path},
460
+ "video_audio": sum_audio_path,
461
+ }
462
+
463
+ print("🎬 Generating video...")
464
+
465
+ # Configure generation parameters based on turbo mode and VRAM availability
466
+ high_vram = (
467
+ torch.cuda.is_available()
468
+ and torch.cuda.get_device_properties(0).total_memory > 40 * 1024**3
469
+ )
470
 
471
+ if turbo:
472
+ teacache_thresh = 0.8
473
+ text_guide_scale = 3.0
474
+ audio_guide_scale = 3.0
475
+ shift = 5.0
476
+ offload_model = False # Never offload in turbo mode
477
+ print(
478
+ f"🚀 TURBO MODE: {sampling_steps} steps, thresh={teacache_thresh}"
479
+ )
480
+ else:
481
+ teacache_thresh = 0.3
482
+ text_guide_scale = 5.0
483
+ audio_guide_scale = 4.0
484
+ shift = 7.0
485
+ offload_model = (
486
+ not high_vram
487
+ ) # Don't offload with high VRAM for maximum speed
488
+ print(
489
+ f"🎬 QUALITY MODE: {sampling_steps} steps{', keeping models in GPU' if high_vram else ''}"
490
+ )
491
+
492
+ # Configure optimizations using SimpleNamespace
493
+ extra_args = SimpleNamespace(
494
+ use_teacache=True,
495
+ teacache_thresh=teacache_thresh,
496
+ use_apg=False,
497
+ size="multitalk-480",
498
+ )
499
 
500
+ # Generate video using loaded pipeline
501
+ video = self.wan_i2v.generate(
502
+ input_data,
503
+ size_buckget="multitalk-480",
504
+ motion_frame=25,
505
+ frame_num=num_frames,
506
+ shift=shift,
507
+ sampling_steps=sampling_steps,
508
+ text_guide_scale=text_guide_scale,
509
+ audio_guide_scale=audio_guide_scale,
510
+ seed=seed,
511
+ offload_model=offload_model,
512
+ max_frames_num=num_frames,
513
+ extra_args=extra_args,
514
+ )
515
 
516
+ # Save video
517
+ output_name = f"multitalk_{abs(hash(prompt + str(seed))) % 10000}"
518
+ print(f"💾 Saving video...")
519
+ save_video_ffmpeg(video, output_name, [input_data["video_audio"]])
520
+
521
+ # Find generated video
522
+ output_file = f"{output_name}.mp4"
523
+ if not os.path.exists(output_file):
524
+ # Look for any mp4 files with our output name
525
+ for file in os.listdir("."):
526
+ if output_name in file and file.endswith(".mp4"):
527
+ output_file = file
528
+ break
529
+
530
+ if not os.path.exists(output_file):
531
+ raise RuntimeError(
532
+ f"Video generation failed - output file not found"
533
+ )
534
+
535
+ # Encode video to base64
536
+ video_b64 = encode_file_to_base64(output_file)
537
+
538
+ # Cleanup GPU memory
539
+ if torch.cuda.is_available():
540
+ torch.cuda.empty_cache()
541
+
542
+ print(f"✅ Video generation completed successfully!")
543
+
544
+ return {
545
+ "video": video_b64,
546
+ "metadata": {
547
+ "seed": seed,
548
+ "num_frames": num_frames,
549
+ "sampling_steps": sampling_steps,
550
+ "turbo": turbo,
551
+ "is_multi_person": is_multi_person,
552
+ "prompt": prompt,
553
+ "original_frames_requested": original_frames,
554
+ },
555
+ }
556
 
557
  except Exception as e:
558
+ print(f"❌ Error during inference: {str(e)}")
559
+ return {"error": str(e), "type": type(e).__name__}
handler_norun.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import json
6
+ import tempfile
7
+ import base64
8
+ from typing import Dict, Any, Optional, List
9
+ import soundfile as sf
10
+ from datetime import datetime
11
+ from huggingface_hub import snapshot_download
12
+
13
+
14
+ class EndpointHandler:
15
+ def __init__(self, path=""):
16
+ """
17
+ Initialize the handler for Meigen MultiTalk inference.
18
+ Optimized for serverless deployment with pre-downloaded models.
19
+
20
+ Args:
21
+ path: Path to the model directory (not used in this implementation)
22
+ """
23
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
24
+
25
+ # Set up the script path - try multiple possible locations
26
+ self.script_path = self.find_script_path()
27
+ if not self.script_path:
28
+ raise RuntimeError("Could not locate generate_multitalk.py script")
29
+
30
+ print(f"Found script at: {self.script_path}")
31
+
32
+ self.setup_models()
33
+ self.setup_gpu_config()
34
+
35
+ def find_script_path(self) -> Optional[str]:
36
+ """
37
+ Find the location of the generate_multitalk.py script.
38
+
39
+ Returns:
40
+ Path to the script if found, None otherwise
41
+ """
42
+ possible_paths = [
43
+ "generate_multitalk.py", # Current directory
44
+ "/app/generate_multitalk.py", # App directory
45
+ "./src/generate_multitalk.py", # Src directory
46
+ "../generate_multitalk.py", # Parent directory
47
+ "./weights/MeiGen-MultiTalk/generate_multitalk.py", # In model directory
48
+ "./MeiGen-MultiTalk/generate_multitalk.py", # Alternative model directory
49
+ ]
50
+
51
+ for path in possible_paths:
52
+ if os.path.exists(path):
53
+ return os.path.abspath(path)
54
+
55
+ # Try to find it recursively in common directories
56
+ search_dirs = ["/app", ".", "./weights", "./src"]
57
+ for search_dir in search_dirs:
58
+ if os.path.exists(search_dir):
59
+ for root, dirs, files in os.walk(search_dir):
60
+ if "generate_multitalk.py" in files:
61
+ return os.path.join(root, "generate_multitalk.py")
62
+
63
+ return None
64
+
65
+ def setup_models(self):
66
+ """Setup models - download only if not already present."""
67
+ print("Setting up models...")
68
+
69
+ # Define model paths
70
+ self.wan_model_path = "./weights/Wan2.1-I2V-14B-480P"
71
+ self.wav2vec_path = "./weights/chinese-wav2vec2-base"
72
+ self.multitalk_path = "./weights/MeiGen-MultiTalk"
73
+
74
+ # Only download if models don't exist (for local development)
75
+ # In serverless deployment, these should be pre-baked into the container
76
+ if not os.path.exists(self.wan_model_path):
77
+ print("Downloading Wan2.1-I2V-14B-480P model...")
78
+ self.wan_model_path = snapshot_download(
79
+ repo_id="Wan-AI/Wan2.1-I2V-14B-480P",
80
+ local_dir=self.wan_model_path,
81
+ )
82
+
83
+ if not os.path.exists(self.wav2vec_path):
84
+ print("Downloading Chinese wav2vec2 model...")
85
+ self.wav2vec_path = snapshot_download(
86
+ repo_id="TencentGameMate/chinese-wav2vec2-base",
87
+ local_dir=self.wav2vec_path,
88
+ )
89
+
90
+ if not os.path.exists(self.multitalk_path):
91
+ print("Downloading MeiGen MultiTalk weights...")
92
+ self.multitalk_path = snapshot_download(
93
+ repo_id="MeiGen-AI/MeiGen-MultiTalk",
94
+ local_dir=self.multitalk_path,
95
+ )
96
+
97
+ # Setup model files if not already done
98
+ self.setup_model_files()
99
+
100
+ def setup_model_files(self):
101
+ """Setup model files by copying and renaming as needed."""
102
+ base_model_dir = self.wan_model_path
103
+ multitalk_dir = self.multitalk_path
104
+
105
+ # Check if setup is already done
106
+ multitalk_weights_path = os.path.join(base_model_dir, "multitalk.safetensors")
107
+ if os.path.exists(multitalk_weights_path):
108
+ print("Model files already set up, skipping setup.")
109
+ return
110
+
111
+ # File to rename
112
+ original_index = os.path.join(
113
+ base_model_dir, "diffusion_pytorch_model.safetensors.index.json"
114
+ )
115
+ backup_index = os.path.join(
116
+ base_model_dir, "diffusion_pytorch_model.safetensors.index.json_old"
117
+ )
118
+
119
+ # Rename the original index file
120
+ if os.path.exists(original_index) and not os.path.exists(backup_index):
121
+ os.rename(original_index, backup_index)
122
+ print("Renamed original index file to .json_old")
123
+
124
+ # Copy updated index file from MultiTalk
125
+ multitalk_index = os.path.join(
126
+ multitalk_dir, "diffusion_pytorch_model.safetensors.index.json"
127
+ )
128
+ if os.path.exists(multitalk_index):
129
+ shutil.copy2(multitalk_index, base_model_dir)
130
+
131
+ # Copy MultiTalk model weights
132
+ multitalk_weights_src = os.path.join(multitalk_dir, "multitalk.safetensors")
133
+ if os.path.exists(multitalk_weights_src):
134
+ shutil.copy2(multitalk_weights_src, base_model_dir)
135
+
136
+ print("Copied MultiTalk files into base model directory.")
137
+
138
+ def setup_gpu_config(self):
139
+ """Setup GPU configuration and VRAM parameters."""
140
+ self.num_gpus = torch.cuda.device_count()
141
+ print(f"GPU AVAILABLE: {self.num_gpus}")
142
+
143
+ if torch.cuda.is_available():
144
+ self.gpu_name = torch.cuda.get_device_name(torch.cuda.current_device())
145
+ print(f"Current GPU: {self.gpu_name}")
146
+
147
+ # GPU to VRAM mapping
148
+ GPU_TO_VRAM_PARAMS = {
149
+ "NVIDIA A100": 11000000000,
150
+ "NVIDIA A100-SXM4-40GB": 11000000000,
151
+ "NVIDIA A100-SXM4-80GB": 22000000000,
152
+ "NVIDIA L4": 5000000000,
153
+ "NVIDIA L40S": 11000000000,
154
+ }
155
+
156
+ self.used_vram_params = GPU_TO_VRAM_PARAMS.get(self.gpu_name, 5000000000)
157
+ print(f"Using {self.used_vram_params} for num_persistent_param_in_dit")
158
+ else:
159
+ raise RuntimeError("No CUDA-compatible GPU found. A GPU is required.")
160
+
161
+ def trim_audio_to_5s_temp(self, audio_path: str, sample_rate: int = 16000) -> str:
162
+ """
163
+ Trim audio to 5 seconds and save to temporary file.
164
+
165
+ Args:
166
+ audio_path: Path to input audio file
167
+ sample_rate: Target sample rate
168
+
169
+ Returns:
170
+ Path to trimmed audio file
171
+ """
172
+ max_duration_sec = 5
173
+ audio, sr = sf.read(audio_path)
174
+
175
+ if sr != sample_rate:
176
+ sample_rate = sr
177
+
178
+ max_samples = max_duration_sec * sample_rate
179
+ if len(audio) > max_samples:
180
+ audio = audio[:max_samples]
181
+
182
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
183
+ base_name = os.path.splitext(os.path.basename(audio_path))[0]
184
+ temp_filename = f"{base_name}_trimmed_{timestamp}.wav"
185
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
186
+
187
+ sf.write(temp_path, audio, samplerate=sample_rate)
188
+ return temp_path
189
+
190
+ def save_base64_file(self, base64_data: str, file_extension: str) -> str:
191
+ """
192
+ Save base64 encoded file to temporary location.
193
+
194
+ Args:
195
+ base64_data: Base64 encoded file data
196
+ file_extension: File extension (e.g., '.png', '.wav')
197
+
198
+ Returns:
199
+ Path to saved file
200
+ """
201
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
202
+ temp_filename = f"temp_file_{timestamp}{file_extension}"
203
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
204
+
205
+ with open(temp_path, "wb") as f:
206
+ f.write(base64.b64decode(base64_data))
207
+
208
+ return temp_path
209
+
210
+ def create_temp_input_json(
211
+ self,
212
+ prompt: str,
213
+ cond_image_path: str,
214
+ cond_audio_path_spk1: str,
215
+ cond_audio_path_spk2: Optional[str] = None,
216
+ ) -> str:
217
+ """
218
+ Create a temporary JSON file with the user-provided prompt, image, and audio paths.
219
+
220
+ Args:
221
+ prompt: Text prompt for generation
222
+ cond_image_path: Path to conditioning image
223
+ cond_audio_path_spk1: Path to first speaker audio
224
+ cond_audio_path_spk2: Path to second speaker audio (optional)
225
+
226
+ Returns:
227
+ Path to temporary JSON file
228
+ """
229
+ if cond_audio_path_spk2 is None:
230
+ data = {
231
+ "prompt": prompt,
232
+ "cond_image": cond_image_path,
233
+ "cond_audio": {"person1": cond_audio_path_spk1},
234
+ }
235
+ else:
236
+ data = {
237
+ "prompt": prompt,
238
+ "cond_image": cond_image_path,
239
+ "audio_type": "para",
240
+ "cond_audio": {
241
+ "person1": cond_audio_path_spk1,
242
+ "person2": cond_audio_path_spk2,
243
+ },
244
+ }
245
+
246
+ temp_json = tempfile.NamedTemporaryFile(
247
+ delete=False, suffix=".json", mode="w", encoding="utf-8"
248
+ )
249
+ json.dump(data, temp_json, indent=4)
250
+ temp_json_path = temp_json.name
251
+ temp_json.close()
252
+
253
+ print(f"Temporary input JSON saved to: {temp_json_path}")
254
+ return temp_json_path
255
+
256
+ def run_inference(
257
+ self,
258
+ prompt: str,
259
+ cond_image_path: str,
260
+ cond_audio_path_spk1: str,
261
+ cond_audio_path_spk2: Optional[str] = None,
262
+ sample_steps: int = 12,
263
+ ) -> str:
264
+ """
265
+ Run the MultiTalk inference.
266
+
267
+ Args:
268
+ prompt: Text prompt
269
+ cond_image_path: Path to conditioning image
270
+ cond_audio_path_spk1: Path to first speaker audio
271
+ cond_audio_path_spk2: Path to second speaker audio (optional)
272
+ sample_steps: Number of sampling steps
273
+
274
+ Returns:
275
+ Path to generated video
276
+ """
277
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
278
+ result_filename = f"meigen_multitalk_result_{sample_steps}_steps_{timestamp}"
279
+ temp_files_to_cleanup = []
280
+
281
+ try:
282
+ # Trim audio files to 5 seconds
283
+ trimmed_audio_path_spk1 = self.trim_audio_to_5s_temp(cond_audio_path_spk1)
284
+ if trimmed_audio_path_spk1 != cond_audio_path_spk1:
285
+ cond_audio_path_spk1 = trimmed_audio_path_spk1
286
+ temp_files_to_cleanup.append(trimmed_audio_path_spk1)
287
+
288
+ if cond_audio_path_spk2 is not None:
289
+ trimmed_audio_path_spk2 = self.trim_audio_to_5s_temp(
290
+ cond_audio_path_spk2
291
+ )
292
+ if trimmed_audio_path_spk2 != cond_audio_path_spk2:
293
+ cond_audio_path_spk2 = trimmed_audio_path_spk2
294
+ temp_files_to_cleanup.append(trimmed_audio_path_spk2)
295
+
296
+ # Prepare input JSON
297
+ input_json_path = self.create_temp_input_json(
298
+ prompt, cond_image_path, cond_audio_path_spk1, cond_audio_path_spk2
299
+ )
300
+ temp_files_to_cleanup.append(input_json_path)
301
+
302
+ # Base command arguments
303
+ common_args = [
304
+ "--ckpt_dir",
305
+ self.wan_model_path,
306
+ "--wav2vec_dir",
307
+ self.wav2vec_path,
308
+ "--input_json",
309
+ input_json_path,
310
+ "--sample_steps",
311
+ str(sample_steps),
312
+ "--mode",
313
+ "streaming",
314
+ "--use_teacache",
315
+ "--save_file",
316
+ result_filename,
317
+ ]
318
+
319
+ # Get the directory containing the script to set as working directory
320
+ script_dir = os.path.dirname(self.script_path)
321
+ script_name = os.path.basename(self.script_path)
322
+
323
+ # Choose command based on number of GPUs
324
+ if self.num_gpus > 1:
325
+ cmd = [
326
+ "torchrun",
327
+ f"--nproc_per_node={self.num_gpus}",
328
+ "--standalone",
329
+ script_name, # Use just the script name since we'll set cwd
330
+ "--dit_fsdp",
331
+ "--t5_fsdp",
332
+ "--ulysses_size",
333
+ str(self.num_gpus),
334
+ ] + common_args
335
+ else:
336
+ cmd = [
337
+ "python3",
338
+ script_name, # Use just the script name since we'll set cwd
339
+ "--num_persistent_param_in_dit",
340
+ str(self.used_vram_params),
341
+ ] + common_args
342
+
343
+ print(f"Running command: {' '.join(cmd)}")
344
+ print(f"Working directory: {script_dir}")
345
+
346
+ # Run inference with correct working directory
347
+ with open("inference.log", "w") as log_file:
348
+ process = subprocess.Popen(
349
+ cmd,
350
+ cwd=script_dir, # Set working directory to script location
351
+ stdout=subprocess.PIPE,
352
+ stderr=subprocess.STDOUT,
353
+ text=True,
354
+ bufsize=1,
355
+ )
356
+ for line in process.stdout:
357
+ print(line, end="")
358
+ log_file.write(line)
359
+ process.wait()
360
+
361
+ if process.returncode != 0:
362
+ raise RuntimeError("Inference failed. Check inference.log for details.")
363
+
364
+ # The output video might be in the script directory
365
+ output_video_path = os.path.join(script_dir, f"{result_filename}.mp4")
366
+ if not os.path.exists(output_video_path):
367
+ # Try current directory as fallback
368
+ output_video_path = f"{result_filename}.mp4"
369
+
370
+ if not os.path.exists(output_video_path):
371
+ raise RuntimeError(f"Output video not found: {output_video_path}")
372
+
373
+ return output_video_path
374
+
375
+ finally:
376
+ # Clean up temporary files
377
+ for f in temp_files_to_cleanup:
378
+ try:
379
+ if os.path.exists(f):
380
+ os.remove(f)
381
+ print(f"[INFO] Removed temporary file: {f}")
382
+ except Exception as e:
383
+ print(f"[WARNING] Could not remove {f}: {e}")
384
+
385
+ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
386
+ """
387
+ Main inference method called by Hugging Face Inference API.
388
+
389
+ Args:
390
+ data: Input data dictionary containing:
391
+ - inputs: Main input data
392
+ - parameters: Optional parameters
393
+
394
+ Returns:
395
+ Dictionary containing the generated video
396
+ """
397
+ try:
398
+ # Extract inputs
399
+ inputs = data.get("inputs", {})
400
+ parameters = data.get("parameters", {})
401
+
402
+ # Required inputs
403
+ prompt = inputs.get("prompt", "")
404
+ if not prompt:
405
+ raise ValueError("Prompt is required")
406
+
407
+ # Image input (base64 encoded)
408
+ image_data = inputs.get("image")
409
+ if not image_data:
410
+ raise ValueError("Image is required")
411
+
412
+ # Audio inputs (base64 encoded)
413
+ audio_spk1_data = inputs.get("audio_spk1")
414
+ if not audio_spk1_data:
415
+ raise ValueError("Audio for speaker 1 is required")
416
+
417
+ audio_spk2_data = inputs.get("audio_spk2") # Optional
418
+
419
+ # Parameters
420
+ sample_steps = parameters.get("sample_steps", 12)
421
+ sample_steps = max(2, min(25, sample_steps)) # Clamp between 2 and 25
422
+
423
+ # Save base64 files to temporary locations
424
+ temp_files_to_cleanup = []
425
+
426
+ # Save image
427
+ image_path = self.save_base64_file(image_data, ".png")
428
+ temp_files_to_cleanup.append(image_path)
429
+
430
+ # Save audio files
431
+ audio_spk1_path = self.save_base64_file(audio_spk1_data, ".wav")
432
+ temp_files_to_cleanup.append(audio_spk1_path)
433
+
434
+ audio_spk2_path = None
435
+ if audio_spk2_data:
436
+ audio_spk2_path = self.save_base64_file(audio_spk2_data, ".wav")
437
+ temp_files_to_cleanup.append(audio_spk2_path)
438
+
439
+ # Run inference
440
+ output_video_path = self.run_inference(
441
+ prompt=prompt,
442
+ cond_image_path=image_path,
443
+ cond_audio_path_spk1=audio_spk1_path,
444
+ cond_audio_path_spk2=audio_spk2_path,
445
+ sample_steps=sample_steps,
446
+ )
447
+
448
+ # Read and encode output video
449
+ with open(output_video_path, "rb") as f:
450
+ video_data = base64.b64encode(f.read()).decode()
451
+
452
+ # Clean up input files
453
+ for f in temp_files_to_cleanup:
454
+ try:
455
+ if os.path.exists(f):
456
+ os.remove(f)
457
+ except Exception as e:
458
+ print(f"[WARNING] Could not remove {f}: {e}")
459
+
460
+ # Clean up output video
461
+ try:
462
+ if os.path.exists(output_video_path):
463
+ os.remove(output_video_path)
464
+ except Exception as e:
465
+ print(f"[WARNING] Could not remove output video: {e}")
466
+
467
+ return {"generated_video": video_data, "content_type": "video/mp4"}
468
+
469
+ except Exception as e:
470
+ return {"error": str(e)}
handler._runningpy → handler_runningpy RENAMED
File without changes
requirements.txt CHANGED
@@ -1,11 +1,6 @@
1
- torch==2.6.0
2
- torchvision==0.21.0
3
- torchaudio==2.6.0
4
  opencv-python>=4.9.0.80
5
  diffusers>=0.31.0
6
  transformers>=4.49.0
7
- peft
8
- xformers
9
  tokenizers>=0.20.3
10
  accelerate>=1.1.1
11
  tqdm
@@ -18,11 +13,15 @@ gradio>=5.0.0
18
  numpy>=1.23.5,<2
19
  xfuser>=0.4.1
20
  pyloudnorm
21
- soundfile
22
- librosa
23
- einops
24
- numpy<2
 
 
 
 
25
  ninja
26
- psutil
27
  packaging
 
28
  https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
 
 
 
 
1
  opencv-python>=4.9.0.80
2
  diffusers>=0.31.0
3
  transformers>=4.49.0
 
 
4
  tokenizers>=0.20.3
5
  accelerate>=1.1.1
6
  tqdm
 
13
  numpy>=1.23.5,<2
14
  xfuser>=0.4.1
15
  pyloudnorm
16
+ torch>=2.4.0
17
+ torchvision>=0.19.0
18
+ torchaudio>=2.4.0
19
+ soundfile>=0.12.1
20
+ librosa>=0.10.0
21
+ einops>=0.8.0
22
+ huggingface-hub>=0.19.0
23
+ xformers>=0.0.28
24
  ninja
 
25
  packaging
26
+ safetensors>=0.3.1
27
  https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
requirements_bck.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.6.0
2
+ torchvision==0.21.0
3
+ torchaudio==2.6.0
4
+ opencv-python>=4.9.0.80
5
+ diffusers>=0.31.0
6
+ transformers>=4.49.0
7
+ peft
8
+ xformers
9
+ tokenizers>=0.20.3
10
+ accelerate>=1.1.1
11
+ tqdm
12
+ imageio
13
+ easydict
14
+ ftfy
15
+ dashscope
16
+ imageio-ffmpeg
17
+ gradio>=5.0.0
18
+ numpy>=1.23.5,<2
19
+ xfuser>=0.4.1
20
+ pyloudnorm
21
+ soundfile
22
+ librosa
23
+ einops
24
+ numpy<2
25
+ ninja
26
+ psutil
27
+ packaging
28
+ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl