J Z commited on
Update float_lipsync.py
Browse files- float_lipsync.py +21 -2
float_lipsync.py
CHANGED
|
@@ -229,6 +229,7 @@ class FloatLipsync:
|
|
| 229 |
img = self._crop_face(img)
|
| 230 |
|
| 231 |
self.preprocessed_ref_image = self.transform(image=img)['image'].unsqueeze(0).to(self.device)
|
|
|
|
| 232 |
logger.info(f"[FLOAT] Reference image preprocessed: {self.preprocessed_ref_image.shape}")
|
| 233 |
|
| 234 |
def _crop_face(self, img: np.ndarray) -> np.ndarray:
|
|
@@ -273,7 +274,7 @@ class FloatLipsync:
|
|
| 273 |
return False
|
| 274 |
|
| 275 |
@torch.no_grad()
|
| 276 |
-
def generate(self, audio_path: str, output_path: str = None, emo: str = 'S2E') -> str:
|
| 277 |
"""
|
| 278 |
Generate a lipsync video from an audio file.
|
| 279 |
|
|
@@ -281,6 +282,7 @@ class FloatLipsync:
|
|
| 281 |
audio_path: Path to the input WAV audio file
|
| 282 |
output_path: Where to save the output video (auto-generated if None)
|
| 283 |
emo: Emotion mode ('S2E' for speech-to-emotion auto-detect)
|
|
|
|
| 284 |
|
| 285 |
Returns:
|
| 286 |
Path to the generated video file
|
|
@@ -298,6 +300,11 @@ class FloatLipsync:
|
|
| 298 |
|
| 299 |
# 1. Load and preprocess audio
|
| 300 |
speech_array, sr = librosa.load(audio_path, sr=self.opt.sampling_rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
processed_audio = self.wav2vec_preprocessor(
|
| 302 |
speech_array, sampling_rate=sr, return_tensors='pt'
|
| 303 |
).input_values[0].unsqueeze(0).to(self.device)
|
|
@@ -324,13 +331,25 @@ class FloatLipsync:
|
|
| 324 |
)['d_hat']
|
| 325 |
logger.info(f"[FLOAT] Model inference: {time.time() - gen_start:.2f}s")
|
| 326 |
|
| 327 |
-
# 3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
self._save_video(d_hat, output_path, audio_path)
|
| 329 |
|
| 330 |
logger.info(f"[FLOAT] ✓ Total generation: {time.time() - inference_start:.2f}s")
|
| 331 |
logger.info(f"[FLOAT] ✓ Output: {output_path}")
|
| 332 |
return output_path
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
def _save_video(self, vid_tensor: torch.Tensor, video_path: str, audio_path: str):
|
| 335 |
"""Save video frames tensor to mp4 with audio using ffmpeg."""
|
| 336 |
# Prepare frames: (T, C, H, W) -> (T, H, W, C) uint8
|
|
|
|
| 229 |
img = self._crop_face(img)
|
| 230 |
|
| 231 |
self.preprocessed_ref_image = self.transform(image=img)['image'].unsqueeze(0).to(self.device)
|
| 232 |
+
self._original_ref_image = self.preprocessed_ref_image.clone()
|
| 233 |
logger.info(f"[FLOAT] Reference image preprocessed: {self.preprocessed_ref_image.shape}")
|
| 234 |
|
| 235 |
def _crop_face(self, img: np.ndarray) -> np.ndarray:
|
|
|
|
| 274 |
return False
|
| 275 |
|
| 276 |
@torch.no_grad()
|
| 277 |
+
def generate(self, audio_path: str, output_path: str = None, emo: str = 'S2E', chain_frames: bool = False) -> str:
|
| 278 |
"""
|
| 279 |
Generate a lipsync video from an audio file.
|
| 280 |
|
|
|
|
| 282 |
audio_path: Path to the input WAV audio file
|
| 283 |
output_path: Where to save the output video (auto-generated if None)
|
| 284 |
emo: Emotion mode ('S2E' for speech-to-emotion auto-detect)
|
| 285 |
+
chain_frames: If True, use the last frame as reference for next call
|
| 286 |
|
| 287 |
Returns:
|
| 288 |
Path to the generated video file
|
|
|
|
| 300 |
|
| 301 |
# 1. Load and preprocess audio
|
| 302 |
speech_array, sr = librosa.load(audio_path, sr=self.opt.sampling_rate)
|
| 303 |
+
|
| 304 |
+
# Pad with 0.5s silence so FLOAT eases the face back to neutral at the end
|
| 305 |
+
pad_samples = int(0.5 * sr)
|
| 306 |
+
speech_array = np.concatenate([speech_array, np.zeros(pad_samples, dtype=speech_array.dtype)])
|
| 307 |
+
|
| 308 |
processed_audio = self.wav2vec_preprocessor(
|
| 309 |
speech_array, sampling_rate=sr, return_tensors='pt'
|
| 310 |
).input_values[0].unsqueeze(0).to(self.device)
|
|
|
|
| 331 |
)['d_hat']
|
| 332 |
logger.info(f"[FLOAT] Model inference: {time.time() - gen_start:.2f}s")
|
| 333 |
|
| 334 |
+
# 3. Chain frames: use last frame as reference for next chunk
|
| 335 |
+
if chain_frames and d_hat.shape[0] > 0:
|
| 336 |
+
# d_hat is (T, C, H, W) in [-1, 1] range — same as preprocessed_ref_image
|
| 337 |
+
self.preprocessed_ref_image = d_hat[-1:].clone().to(self.device)
|
| 338 |
+
logger.info(f"[FLOAT] Chained last frame as next reference")
|
| 339 |
+
|
| 340 |
+
# 4. Save video with audio
|
| 341 |
self._save_video(d_hat, output_path, audio_path)
|
| 342 |
|
| 343 |
logger.info(f"[FLOAT] ✓ Total generation: {time.time() - inference_start:.2f}s")
|
| 344 |
logger.info(f"[FLOAT] ✓ Output: {output_path}")
|
| 345 |
return output_path
|
| 346 |
|
| 347 |
+
def reset_reference(self):
|
| 348 |
+
"""Reset the reference image back to the original ref.png."""
|
| 349 |
+
if hasattr(self, '_original_ref_image') and self._original_ref_image is not None:
|
| 350 |
+
self.preprocessed_ref_image = self._original_ref_image.clone()
|
| 351 |
+
logger.info("[FLOAT] Reference reset to original")
|
| 352 |
+
|
| 353 |
def _save_video(self, vid_tensor: torch.Tensor, video_path: str, audio_path: str):
|
| 354 |
"""Save video frames tensor to mp4 with audio using ffmpeg."""
|
| 355 |
# Prepare frames: (T, C, H, W) -> (T, H, W, C) uint8
|