Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -60,7 +60,6 @@ def download_weights():
|
|
| 60 |
"""
|
| 61 |
Downloads pre-trained weights from Hugging Face Hub if they don't exist locally.
|
| 62 |
"""
|
| 63 |
-
# A simple check for a key file to see if the download is likely complete
|
| 64 |
motion_model_file = os.path.join(WEIGHTS_DIR, "moda", "net-200.pth")
|
| 65 |
|
| 66 |
if not os.path.exists(motion_model_file):
|
|
@@ -97,20 +96,16 @@ def ensure_wav_format(audio_path, max_duration_ms=300000): # Increased to 5 minu
|
|
| 97 |
|
| 98 |
try:
|
| 99 |
print(f"Loading and processing audio from: {audio_path}")
|
| 100 |
-
# Load the audio file (supports wav, mp3, m4a, etc.)
|
| 101 |
audio = AudioSegment.from_file(audio_path)
|
| 102 |
|
| 103 |
-
# Trim if the audio exceeds the limit
|
| 104 |
if len(audio) > max_duration_ms:
|
| 105 |
print(f"Audio is {len(audio)/1000:.2f}s, trimming to {max_duration_ms/1000:.2f}s")
|
| 106 |
audio = audio[:max_duration_ms]
|
| 107 |
else:
|
| 108 |
print(f"Audio is {len(audio)/1000:.2f}s (under the {max_duration_ms/1000:.2f}s limit)")
|
| 109 |
|
| 110 |
-
# Create a temporary WAV file
|
| 111 |
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
|
| 112 |
wav_path = tmp_file.name
|
| 113 |
-
# Export as WAV with standard settings
|
| 114 |
audio.export(
|
| 115 |
wav_path,
|
| 116 |
format='wav',
|
|
@@ -152,62 +147,57 @@ def extract_last_frame(video_path, output_image_path):
|
|
| 152 |
|
| 153 |
def concatenate_videos_ffmpeg(video_paths, full_audio_path, output_path):
|
| 154 |
"""
|
| 155 |
-
Stitches multiple video files together
|
| 156 |
-
|
|
|
|
| 157 |
"""
|
| 158 |
-
# Create the text file listing all videos
|
| 159 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
|
| 160 |
-
for p in video_paths:
|
| 161 |
-
# Format absolute path with forward slashes and escape single quotes
|
| 162 |
-
abs_path = os.path.abspath(p).replace('\\', '/')
|
| 163 |
-
safe_path = abs_path.replace("'", "'\\''")
|
| 164 |
-
f.write(f"file '{safe_path}'\n")
|
| 165 |
-
list_file_path = f.name
|
| 166 |
-
|
| 167 |
temp_video_only = os.path.join(os.path.dirname(output_path), "temp_video_only.mp4")
|
|
|
|
| 168 |
|
| 169 |
try:
|
| 170 |
-
#
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
'-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
temp_video_only
|
| 179 |
-
]
|
| 180 |
-
print(f"Executing lossless FFMPEG video concat (no-audio): {' '.join(cmd_copy)}")
|
| 181 |
-
result = subprocess.run(cmd_copy, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
'ffmpeg', '-y',
|
| 188 |
-
'-f', 'concat',
|
| 189 |
-
'-safe', '0',
|
| 190 |
-
'-i', list_file_path,
|
| 191 |
-
'-an',
|
| 192 |
-
'-c:v', 'libx264',
|
| 193 |
-
'-pix_fmt', 'yuv420p',
|
| 194 |
-
temp_video_only
|
| 195 |
-
]
|
| 196 |
-
print(f"Executing FFMPEG video-only re-encode: {' '.join(cmd_reencode)}")
|
| 197 |
-
result_re = subprocess.run(cmd_reencode, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 198 |
-
if result_re.returncode != 0:
|
| 199 |
-
raise Exception(f"FFMPEG video concatenation failed: {result_re.stderr.decode('utf-8')}")
|
| 200 |
|
| 201 |
-
# Step 2: Merge the
|
| 202 |
cmd_merge = [
|
| 203 |
'ffmpeg', '-y',
|
| 204 |
'-i', temp_video_only,
|
| 205 |
'-i', str(full_audio_path),
|
| 206 |
-
'-map', '0:v:0', #
|
| 207 |
-
'-map', '1:a:0', #
|
| 208 |
-
'-c:v', 'copy', #
|
| 209 |
'-c:a', 'aac', # Encode original audio to standard clean AAC
|
| 210 |
-
'-shortest', # Sync to
|
| 211 |
str(output_path)
|
| 212 |
]
|
| 213 |
print(f"Applying final original audio track: {' '.join(cmd_merge)}")
|
|
@@ -216,9 +206,7 @@ def concatenate_videos_ffmpeg(video_paths, full_audio_path, output_path):
|
|
| 216 |
raise Exception(f"FFMPEG audio replacement failed: {result_merge.stderr.decode('utf-8')}")
|
| 217 |
|
| 218 |
finally:
|
| 219 |
-
# Clean up temporary
|
| 220 |
-
if os.path.exists(list_file_path):
|
| 221 |
-
os.remove(list_file_path)
|
| 222 |
if os.path.exists(temp_video_only):
|
| 223 |
try:
|
| 224 |
os.remove(temp_video_only)
|
|
@@ -251,8 +239,8 @@ emo_name_to_id = {v: k for k, v in emo_map.items()}
|
|
| 251 |
@spaces.GPU(duration=120)
|
| 252 |
def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_scale, progress=gr.Progress(track_tqdm=True)):
|
| 253 |
"""
|
| 254 |
-
Generates talking head video. For inputs longer than
|
| 255 |
-
chunks the audio into
|
| 256 |
last frame of the previous block as the next source image, and then merges them.
|
| 257 |
"""
|
| 258 |
if pipeline is None:
|
|
@@ -284,8 +272,8 @@ def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_sca
|
|
| 284 |
print(f" Emotion: {emotion_name} (ID: {emotion_id})")
|
| 285 |
print(f" CFG Scale: {cfg_scale}")
|
| 286 |
|
| 287 |
-
# Chunk settings
|
| 288 |
-
CHUNK_SIZE_MS =
|
| 289 |
temp_wav_files = []
|
| 290 |
temp_frame_files = []
|
| 291 |
video_chunks_paths = []
|
|
@@ -296,7 +284,7 @@ def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_sca
|
|
| 296 |
audio = AudioSegment.from_file(wav_audio_path)
|
| 297 |
audio_duration_ms = len(audio)
|
| 298 |
|
| 299 |
-
# Slice audio into segments of max
|
| 300 |
audio_chunks = []
|
| 301 |
for i in range(0, audio_duration_ms, CHUNK_SIZE_MS):
|
| 302 |
audio_chunks.append(audio[i : i + CHUNK_SIZE_MS])
|
|
@@ -329,7 +317,7 @@ def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_sca
|
|
| 329 |
cfg_scale=float(cfg_scale),
|
| 330 |
emo=emotion_id,
|
| 331 |
save_dir=run_output_dir,
|
| 332 |
-
smooth=
|
| 333 |
silent_audio_path=DEFAULT_SILENT_AUDIO_PATH,
|
| 334 |
)
|
| 335 |
|
|
@@ -362,12 +350,12 @@ def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_sca
|
|
| 362 |
print("Falling back to the original source image for the next segment.")
|
| 363 |
current_image_path = source_image_path
|
| 364 |
|
| 365 |
-
# 4. Merge all generated chunks and
|
| 366 |
progress(0.95, desc="Merging segments and applying original audio...")
|
| 367 |
final_video_name = f"final_{timestamp}.mp4"
|
| 368 |
final_path = Path(os.path.join(run_output_dir, final_video_name))
|
| 369 |
|
| 370 |
-
#
|
| 371 |
concatenate_videos_ffmpeg(video_chunks_paths, wav_audio_path, final_path)
|
| 372 |
|
| 373 |
except Exception as e:
|
|
|
|
| 60 |
"""
|
| 61 |
Downloads pre-trained weights from Hugging Face Hub if they don't exist locally.
|
| 62 |
"""
|
|
|
|
| 63 |
motion_model_file = os.path.join(WEIGHTS_DIR, "moda", "net-200.pth")
|
| 64 |
|
| 65 |
if not os.path.exists(motion_model_file):
|
|
|
|
| 96 |
|
| 97 |
try:
|
| 98 |
print(f"Loading and processing audio from: {audio_path}")
|
|
|
|
| 99 |
audio = AudioSegment.from_file(audio_path)
|
| 100 |
|
|
|
|
| 101 |
if len(audio) > max_duration_ms:
|
| 102 |
print(f"Audio is {len(audio)/1000:.2f}s, trimming to {max_duration_ms/1000:.2f}s")
|
| 103 |
audio = audio[:max_duration_ms]
|
| 104 |
else:
|
| 105 |
print(f"Audio is {len(audio)/1000:.2f}s (under the {max_duration_ms/1000:.2f}s limit)")
|
| 106 |
|
|
|
|
| 107 |
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
|
| 108 |
wav_path = tmp_file.name
|
|
|
|
| 109 |
audio.export(
|
| 110 |
wav_path,
|
| 111 |
format='wav',
|
|
|
|
| 147 |
|
| 148 |
def concatenate_videos_ffmpeg(video_paths, full_audio_path, output_path):
|
| 149 |
"""
|
| 150 |
+
Stitches multiple video files together using FFMPEG concat filter (with full re-encoding).
|
| 151 |
+
Standardizes dimensions to 512x512, sets SAR to 1, and forces a strict 25 FPS timebase.
|
| 152 |
+
Finally overlays the clean original audio track.
|
| 153 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
temp_video_only = os.path.join(os.path.dirname(output_path), "temp_video_only.mp4")
|
| 155 |
+
num_inputs = len(video_paths)
|
| 156 |
|
| 157 |
try:
|
| 158 |
+
# Build the FFMPEG command with input files
|
| 159 |
+
cmd_concat = ['ffmpeg', '-y']
|
| 160 |
+
for p in video_paths:
|
| 161 |
+
cmd_concat.extend(['-i', str(p)])
|
| 162 |
+
|
| 163 |
+
# Construct the complex filter: scale to 512x512, set SAR, and concatenate
|
| 164 |
+
filter_parts = []
|
| 165 |
+
concat_inputs = []
|
| 166 |
+
for i in range(num_inputs):
|
| 167 |
+
filter_parts.append(f"[{i}:v]scale=512:512,setsar=1[v{i}]")
|
| 168 |
+
concat_inputs.append(f"[v{i}]")
|
| 169 |
+
|
| 170 |
+
concat_inputs_str = "".join(concat_inputs)
|
| 171 |
+
filter_parts.append(f"{concat_inputs_str}concat=n={num_inputs}:v=1:a=0[v]")
|
| 172 |
+
filter_complex_str = "; ".join(filter_parts)
|
| 173 |
+
|
| 174 |
+
# Output setup for high compatibility (H.264, YUV420p, strict 25 FPS)
|
| 175 |
+
cmd_concat.extend([
|
| 176 |
+
'-filter_complex', filter_complex_str,
|
| 177 |
+
'-map', '[v]',
|
| 178 |
+
'-c:v', 'libx264',
|
| 179 |
+
'-preset', 'medium',
|
| 180 |
+
'-crf', '18', # High quality output
|
| 181 |
+
'-pix_fmt', 'yuv420p', # High compatibility pixel format
|
| 182 |
+
'-r', '25', # Strictly 25 fps matching default output
|
| 183 |
temp_video_only
|
| 184 |
+
])
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
print(f"Executing video concat filter: {' '.join(cmd_concat)}")
|
| 187 |
+
result_concat = subprocess.run(cmd_concat, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 188 |
+
if result_concat.returncode != 0:
|
| 189 |
+
raise Exception(f"FFMPEG video-only concat filter failed: {result_concat.stderr.decode('utf-8')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
+
# Step 2: Merge the newly rendered video with the complete original audio file
|
| 192 |
cmd_merge = [
|
| 193 |
'ffmpeg', '-y',
|
| 194 |
'-i', temp_video_only,
|
| 195 |
'-i', str(full_audio_path),
|
| 196 |
+
'-map', '0:v:0', # Map video from temp file
|
| 197 |
+
'-map', '1:a:0', # Map audio from original full wav file
|
| 198 |
+
'-c:v', 'copy', # Direct copy of the already encoded video
|
| 199 |
'-c:a', 'aac', # Encode original audio to standard clean AAC
|
| 200 |
+
'-shortest', # Sync to shortest stream
|
| 201 |
str(output_path)
|
| 202 |
]
|
| 203 |
print(f"Applying final original audio track: {' '.join(cmd_merge)}")
|
|
|
|
| 206 |
raise Exception(f"FFMPEG audio replacement failed: {result_merge.stderr.decode('utf-8')}")
|
| 207 |
|
| 208 |
finally:
|
| 209 |
+
# Clean up temporary video file
|
|
|
|
|
|
|
| 210 |
if os.path.exists(temp_video_only):
|
| 211 |
try:
|
| 212 |
os.remove(temp_video_only)
|
|
|
|
| 239 |
@spaces.GPU(duration=120)
|
| 240 |
def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_scale, progress=gr.Progress(track_tqdm=True)):
|
| 241 |
"""
|
| 242 |
+
Generates talking head video. For inputs longer than 7 seconds, it automatically
|
| 243 |
+
chunks the audio into 7-second blocks, processes them sequentially using the
|
| 244 |
last frame of the previous block as the next source image, and then merges them.
|
| 245 |
"""
|
| 246 |
if pipeline is None:
|
|
|
|
| 272 |
print(f" Emotion: {emotion_name} (ID: {emotion_id})")
|
| 273 |
print(f" CFG Scale: {cfg_scale}")
|
| 274 |
|
| 275 |
+
# Chunk settings
|
| 276 |
+
CHUNK_SIZE_MS = 7000 # 7 seconds
|
| 277 |
temp_wav_files = []
|
| 278 |
temp_frame_files = []
|
| 279 |
video_chunks_paths = []
|
|
|
|
| 284 |
audio = AudioSegment.from_file(wav_audio_path)
|
| 285 |
audio_duration_ms = len(audio)
|
| 286 |
|
| 287 |
+
# Slice audio into segments of max 7 seconds
|
| 288 |
audio_chunks = []
|
| 289 |
for i in range(0, audio_duration_ms, CHUNK_SIZE_MS):
|
| 290 |
audio_chunks.append(audio[i : i + CHUNK_SIZE_MS])
|
|
|
|
| 317 |
cfg_scale=float(cfg_scale),
|
| 318 |
emo=emotion_id,
|
| 319 |
save_dir=run_output_dir,
|
| 320 |
+
smooth=False, # Disable smoothing for a faster demo
|
| 321 |
silent_audio_path=DEFAULT_SILENT_AUDIO_PATH,
|
| 322 |
)
|
| 323 |
|
|
|
|
| 350 |
print("Falling back to the original source image for the next segment.")
|
| 351 |
current_image_path = source_image_path
|
| 352 |
|
| 353 |
+
# 4. Merge all generated chunks and overlay the original full audio track
|
| 354 |
progress(0.95, desc="Merging segments and applying original audio...")
|
| 355 |
final_video_name = f"final_{timestamp}.mp4"
|
| 356 |
final_path = Path(os.path.join(run_output_dir, final_video_name))
|
| 357 |
|
| 358 |
+
# Force re-encoding merge to ensure absolute smooth transitions without lag
|
| 359 |
concatenate_videos_ffmpeg(video_chunks_paths, wav_audio_path, final_path)
|
| 360 |
|
| 361 |
except Exception as e:
|