vincentraj commited on
Commit
4533bd9
·
1 Parent(s): 7c8d650

Initial Commit

Browse files
Files changed (1) hide show
  1. handler.py +398 -0
handler.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
19
+ Args:
20
+ path: Path to the model directory (not used in this implementation)
21
+ """
22
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
23
+ self.setup_models()
24
+ self.setup_gpu_config()
25
+
26
+ def setup_models(self):
27
+ """Download and setup all required models."""
28
+ print("Setting up models...")
29
+
30
+ # Download Wan2.1-I2V-14B-480P model
31
+ self.wan_model_path = snapshot_download(
32
+ repo_id="Wan-AI/Wan2.1-I2V-14B-480P",
33
+ local_dir="./weights/Wan2.1-I2V-14B-480P",
34
+ )
35
+
36
+ # Download Chinese wav2vec2 model
37
+ self.wav2vec_path = snapshot_download(
38
+ repo_id="TencentGameMate/chinese-wav2vec2-base",
39
+ local_dir="./weights/chinese-wav2vec2-base",
40
+ )
41
+
42
+ # Download MeiGen MultiTalk weights
43
+ self.multitalk_path = snapshot_download(
44
+ repo_id="MeiGen-AI/MeiGen-MultiTalk",
45
+ local_dir="./weights/MeiGen-MultiTalk",
46
+ )
47
+
48
+ self.setup_model_files()
49
+
50
+ def setup_model_files(self):
51
+ """Setup model files by copying and renaming as needed."""
52
+ base_model_dir = "./weights/Wan2.1-I2V-14B-480P"
53
+ multitalk_dir = "./weights/MeiGen-MultiTalk"
54
+
55
+ # File to rename
56
+ original_index = os.path.join(
57
+ base_model_dir, "diffusion_pytorch_model.safetensors.index.json"
58
+ )
59
+ backup_index = os.path.join(
60
+ base_model_dir, "diffusion_pytorch_model.safetensors.index.json_old"
61
+ )
62
+
63
+ # Rename the original index file
64
+ if os.path.exists(original_index):
65
+ os.rename(original_index, backup_index)
66
+ print("Renamed original index file to .json_old")
67
+
68
+ # Copy updated index file from MultiTalk
69
+ shutil.copy2(
70
+ os.path.join(
71
+ multitalk_dir, "diffusion_pytorch_model.safetensors.index.json"
72
+ ),
73
+ base_model_dir,
74
+ )
75
+
76
+ # Copy MultiTalk model weights
77
+ shutil.copy2(
78
+ os.path.join(multitalk_dir, "multitalk.safetensors"), base_model_dir
79
+ )
80
+
81
+ print("Copied MultiTalk files into base model directory.")
82
+
83
+ def setup_gpu_config(self):
84
+ """Setup GPU configuration and VRAM parameters."""
85
+ self.num_gpus = torch.cuda.device_count()
86
+ print(f"GPU AVAILABLE: {self.num_gpus}")
87
+
88
+ if torch.cuda.is_available():
89
+ self.gpu_name = torch.cuda.get_device_name(torch.cuda.current_device())
90
+ print(f"Current GPU: {self.gpu_name}")
91
+
92
+ # GPU to VRAM mapping
93
+ GPU_TO_VRAM_PARAMS = {
94
+ "NVIDIA A100": 11000000000,
95
+ "NVIDIA A100-SXM4-40GB": 11000000000,
96
+ "NVIDIA A100-SXM4-80GB": 22000000000,
97
+ "NVIDIA L4": 5000000000,
98
+ "NVIDIA L40S": 11000000000,
99
+ }
100
+
101
+ self.used_vram_params = GPU_TO_VRAM_PARAMS.get(self.gpu_name, 5000000000)
102
+ print(f"Using {self.used_vram_params} for num_persistent_param_in_dit")
103
+ else:
104
+ raise RuntimeError("No CUDA-compatible GPU found. A GPU is required.")
105
+
106
+ def trim_audio_to_5s_temp(self, audio_path: str, sample_rate: int = 16000) -> str:
107
+ """
108
+ Trim audio to 5 seconds and save to temporary file.
109
+
110
+ Args:
111
+ audio_path: Path to input audio file
112
+ sample_rate: Target sample rate
113
+
114
+ Returns:
115
+ Path to trimmed audio file
116
+ """
117
+ max_duration_sec = 5
118
+ audio, sr = sf.read(audio_path)
119
+
120
+ if sr != sample_rate:
121
+ sample_rate = sr
122
+
123
+ max_samples = max_duration_sec * sample_rate
124
+ if len(audio) > max_samples:
125
+ audio = audio[:max_samples]
126
+
127
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
128
+ base_name = os.path.splitext(os.path.basename(audio_path))[0]
129
+ temp_filename = f"{base_name}_trimmed_{timestamp}.wav"
130
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
131
+
132
+ sf.write(temp_path, audio, samplerate=sample_rate)
133
+ return temp_path
134
+
135
+ def save_base64_file(self, base64_data: str, file_extension: str) -> str:
136
+ """
137
+ Save base64 encoded file to temporary location.
138
+
139
+ Args:
140
+ base64_data: Base64 encoded file data
141
+ file_extension: File extension (e.g., '.png', '.wav')
142
+
143
+ Returns:
144
+ Path to saved file
145
+ """
146
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
147
+ temp_filename = f"temp_file_{timestamp}{file_extension}"
148
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
149
+
150
+ with open(temp_path, "wb") as f:
151
+ f.write(base64.b64decode(base64_data))
152
+
153
+ return temp_path
154
+
155
+ def create_temp_input_json(
156
+ self,
157
+ prompt: str,
158
+ cond_image_path: str,
159
+ cond_audio_path_spk1: str,
160
+ cond_audio_path_spk2: Optional[str] = None,
161
+ ) -> str:
162
+ """
163
+ Create a temporary JSON file with the user-provided prompt, image, and audio paths.
164
+
165
+ Args:
166
+ prompt: Text prompt for generation
167
+ cond_image_path: Path to conditioning image
168
+ cond_audio_path_spk1: Path to first speaker audio
169
+ cond_audio_path_spk2: Path to second speaker audio (optional)
170
+
171
+ Returns:
172
+ Path to temporary JSON file
173
+ """
174
+ if cond_audio_path_spk2 is None:
175
+ data = {
176
+ "prompt": prompt,
177
+ "cond_image": cond_image_path,
178
+ "cond_audio": {"person1": cond_audio_path_spk1},
179
+ }
180
+ else:
181
+ data = {
182
+ "prompt": prompt,
183
+ "cond_image": cond_image_path,
184
+ "audio_type": "para",
185
+ "cond_audio": {
186
+ "person1": cond_audio_path_spk1,
187
+ "person2": cond_audio_path_spk2,
188
+ },
189
+ }
190
+
191
+ temp_json = tempfile.NamedTemporaryFile(
192
+ delete=False, suffix=".json", mode="w", encoding="utf-8"
193
+ )
194
+ json.dump(data, temp_json, indent=4)
195
+ temp_json_path = temp_json.name
196
+ temp_json.close()
197
+
198
+ print(f"Temporary input JSON saved to: {temp_json_path}")
199
+ return temp_json_path
200
+
201
+ def run_inference(
202
+ self,
203
+ prompt: str,
204
+ cond_image_path: str,
205
+ cond_audio_path_spk1: str,
206
+ cond_audio_path_spk2: Optional[str] = None,
207
+ sample_steps: int = 12,
208
+ ) -> str:
209
+ """
210
+ Run the MultiTalk inference.
211
+
212
+ Args:
213
+ prompt: Text prompt
214
+ cond_image_path: Path to conditioning image
215
+ cond_audio_path_spk1: Path to first speaker audio
216
+ cond_audio_path_spk2: Path to second speaker audio (optional)
217
+ sample_steps: Number of sampling steps
218
+
219
+ Returns:
220
+ Path to generated video
221
+ """
222
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
223
+ result_filename = f"meigen_multitalk_result_{sample_steps}_steps_{timestamp}"
224
+ temp_files_to_cleanup = []
225
+
226
+ try:
227
+ # Trim audio files to 5 seconds
228
+ trimmed_audio_path_spk1 = self.trim_audio_to_5s_temp(cond_audio_path_spk1)
229
+ if trimmed_audio_path_spk1 != cond_audio_path_spk1:
230
+ cond_audio_path_spk1 = trimmed_audio_path_spk1
231
+ temp_files_to_cleanup.append(trimmed_audio_path_spk1)
232
+
233
+ if cond_audio_path_spk2 is not None:
234
+ trimmed_audio_path_spk2 = self.trim_audio_to_5s_temp(
235
+ cond_audio_path_spk2
236
+ )
237
+ if trimmed_audio_path_spk2 != cond_audio_path_spk2:
238
+ cond_audio_path_spk2 = trimmed_audio_path_spk2
239
+ temp_files_to_cleanup.append(trimmed_audio_path_spk2)
240
+
241
+ # Prepare input JSON
242
+ input_json_path = self.create_temp_input_json(
243
+ prompt, cond_image_path, cond_audio_path_spk1, cond_audio_path_spk2
244
+ )
245
+ temp_files_to_cleanup.append(input_json_path)
246
+
247
+ # Base command arguments
248
+ common_args = [
249
+ "--ckpt_dir",
250
+ "weights/Wan2.1-I2V-14B-480P",
251
+ "--wav2vec_dir",
252
+ "weights/chinese-wav2vec2-base",
253
+ "--input_json",
254
+ input_json_path,
255
+ "--sample_steps",
256
+ str(sample_steps),
257
+ "--mode",
258
+ "streaming",
259
+ "--use_teacache",
260
+ "--save_file",
261
+ result_filename,
262
+ ]
263
+
264
+ # Choose command based on number of GPUs
265
+ if self.num_gpus > 1:
266
+ cmd = [
267
+ "torchrun",
268
+ f"--nproc_per_node={self.num_gpus}",
269
+ "--standalone",
270
+ "generate_multitalk.py",
271
+ "--dit_fsdp",
272
+ "--t5_fsdp",
273
+ "--ulysses_size",
274
+ str(self.num_gpus),
275
+ ] + common_args
276
+ else:
277
+ cmd = [
278
+ "python3",
279
+ "generate_multitalk.py",
280
+ "--num_persistent_param_in_dit",
281
+ str(self.used_vram_params),
282
+ ] + common_args
283
+
284
+ # Run inference
285
+ with open("inference.log", "w") as log_file:
286
+ process = subprocess.Popen(
287
+ cmd,
288
+ stdout=subprocess.PIPE,
289
+ stderr=subprocess.STDOUT,
290
+ text=True,
291
+ bufsize=1,
292
+ )
293
+ for line in process.stdout:
294
+ print(line, end="")
295
+ log_file.write(line)
296
+ process.wait()
297
+
298
+ if process.returncode != 0:
299
+ raise RuntimeError("Inference failed. Check inference.log for details.")
300
+
301
+ return f"{result_filename}.mp4"
302
+
303
+ finally:
304
+ # Clean up temporary files
305
+ for f in temp_files_to_cleanup:
306
+ try:
307
+ if os.path.exists(f):
308
+ os.remove(f)
309
+ print(f"[INFO] Removed temporary file: {f}")
310
+ except Exception as e:
311
+ print(f"[WARNING] Could not remove {f}: {e}")
312
+
313
+ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
314
+ """
315
+ Main inference method called by Hugging Face Inference API.
316
+
317
+ Args:
318
+ data: Input data dictionary containing:
319
+ - inputs: Main input data
320
+ - parameters: Optional parameters
321
+
322
+ Returns:
323
+ Dictionary containing the generated video
324
+ """
325
+ try:
326
+ # Extract inputs
327
+ inputs = data.get("inputs", {})
328
+ parameters = data.get("parameters", {})
329
+
330
+ # Required inputs
331
+ prompt = inputs.get("prompt", "")
332
+ if not prompt:
333
+ raise ValueError("Prompt is required")
334
+
335
+ # Image input (base64 encoded)
336
+ image_data = inputs.get("image")
337
+ if not image_data:
338
+ raise ValueError("Image is required")
339
+
340
+ # Audio inputs (base64 encoded)
341
+ audio_spk1_data = inputs.get("audio_spk1")
342
+ if not audio_spk1_data:
343
+ raise ValueError("Audio for speaker 1 is required")
344
+
345
+ audio_spk2_data = inputs.get("audio_spk2") # Optional
346
+
347
+ # Parameters
348
+ sample_steps = parameters.get("sample_steps", 12)
349
+ sample_steps = max(2, min(25, sample_steps)) # Clamp between 2 and 25
350
+
351
+ # Save base64 files to temporary locations
352
+ temp_files_to_cleanup = []
353
+
354
+ # Save image
355
+ image_path = self.save_base64_file(image_data, ".png")
356
+ temp_files_to_cleanup.append(image_path)
357
+
358
+ # Save audio files
359
+ audio_spk1_path = self.save_base64_file(audio_spk1_data, ".wav")
360
+ temp_files_to_cleanup.append(audio_spk1_path)
361
+
362
+ audio_spk2_path = None
363
+ if audio_spk2_data:
364
+ audio_spk2_path = self.save_base64_file(audio_spk2_data, ".wav")
365
+ temp_files_to_cleanup.append(audio_spk2_path)
366
+
367
+ # Run inference
368
+ output_video_path = self.run_inference(
369
+ prompt=prompt,
370
+ cond_image_path=image_path,
371
+ cond_audio_path_spk1=audio_spk1_path,
372
+ cond_audio_path_spk2=audio_spk2_path,
373
+ sample_steps=sample_steps,
374
+ )
375
+
376
+ # Read and encode output video
377
+ with open(output_video_path, "rb") as f:
378
+ video_data = base64.b64encode(f.read()).decode()
379
+
380
+ # Clean up input files
381
+ for f in temp_files_to_cleanup:
382
+ try:
383
+ if os.path.exists(f):
384
+ os.remove(f)
385
+ except Exception as e:
386
+ print(f"[WARNING] Could not remove {f}: {e}")
387
+
388
+ # Clean up output video
389
+ try:
390
+ if os.path.exists(output_video_path):
391
+ os.remove(output_video_path)
392
+ except Exception as e:
393
+ print(f"[WARNING] Could not remove output video: {e}")
394
+
395
+ return {"generated_video": video_data, "content_type": "video/mp4"}
396
+
397
+ except Exception as e:
398
+ return {"error": str(e)}