Opera8 commited on
Commit
ae61cc9
·
verified ·
1 Parent(s): 2e1da8f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -42
app.py CHANGED
@@ -55,11 +55,35 @@ OUTPUT_DIR = "gradio_output"
55
  WEIGHTS_DIR = "pretrain_weights"
56
  REPO_ID = "lixinyizju/moda"
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  # --- Download Pre-trained Weights from Hugging Face Hub ---
59
  def download_weights():
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,16 +120,20 @@ def ensure_wav_format(audio_path, max_duration_ms=300000): # Increased to 5 minu
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,57 +175,62 @@ def extract_last_frame(video_path, output_image_path):
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,7 +239,9 @@ def concatenate_videos_ffmpeg(video_paths, full_audio_path, output_path):
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)
@@ -217,6 +252,9 @@ def concatenate_videos_ffmpeg(video_paths, full_audio_path, output_path):
217
  # Create output directory if it doesn't exist
218
  os.makedirs(OUTPUT_DIR, exist_ok=True)
219
 
 
 
 
220
  # Download weights before initializing the pipeline
221
  download_weights()
222
 
@@ -350,12 +388,12 @@ def generate_motion(source_image_path, driving_audio_path, emotion_name, cfg_sca
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:
 
55
  WEIGHTS_DIR = "pretrain_weights"
56
  REPO_ID = "lixinyizju/moda"
57
 
58
+ # --- Automatic Config Patching for Aspect Ratio Preservation ---
59
+ def patch_liveportrait_config():
60
+ """
61
+ Patches the liveportrait_config.yaml to enable 'flag_pasteback'.
62
+ This stitches the animated face back onto the original high-resolution full-sized
63
+ source image, maintaining the exact original image size and aspect ratio.
64
+ """
65
+ config_path = "configs/audio2motion/model/liveportrait_config.yaml"
66
+ if os.path.exists(config_path):
67
+ try:
68
+ import yaml
69
+ with open(config_path, 'r', encoding='utf-8') as f:
70
+ config_data = yaml.safe_load(f) or {}
71
+
72
+ # Enable pasteback to restore original image dimensions in the output video
73
+ if config_data.get('flag_pasteback') is not True:
74
+ config_data['flag_pasteback'] = True
75
+ with open(config_path, 'w', encoding='utf-8') as f:
76
+ yaml.dump(config_data, f)
77
+ print("Successfully patched liveportrait_config.yaml: flag_pasteback set to True")
78
+ except Exception as e:
79
+ print(f"Warning: Failed to automatically patch configuration file: {e}")
80
+
81
  # --- Download Pre-trained Weights from Hugging Face Hub ---
82
  def download_weights():
83
  """
84
  Downloads pre-trained weights from Hugging Face Hub if they don't exist locally.
85
  """
86
+ # A simple check for a key file to see if the download is likely complete
87
  motion_model_file = os.path.join(WEIGHTS_DIR, "moda", "net-200.pth")
88
 
89
  if not os.path.exists(motion_model_file):
 
120
 
121
  try:
122
  print(f"Loading and processing audio from: {audio_path}")
123
+ # Load the audio file (supports wav, mp3, m4a, etc.)
124
  audio = AudioSegment.from_file(audio_path)
125
 
126
+ # Trim if the audio exceeds the limit
127
  if len(audio) > max_duration_ms:
128
  print(f"Audio is {len(audio)/1000:.2f}s, trimming to {max_duration_ms/1000:.2f}s")
129
  audio = audio[:max_duration_ms]
130
  else:
131
  print(f"Audio is {len(audio)/1000:.2f}s (under the {max_duration_ms/1000:.2f}s limit)")
132
 
133
+ # Create a temporary WAV file
134
  with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
135
  wav_path = tmp_file.name
136
+ # Export as WAV with standard settings
137
  audio.export(
138
  wav_path,
139
  format='wav',
 
175
 
176
  def concatenate_videos_ffmpeg(video_paths, full_audio_path, output_path):
177
  """
178
+ Stitches multiple video files together (video track only) and overlays the original full audio track.
179
+ This bypasses any pops or sync problems at the chunk boundaries.
 
180
  """
181
+ # Create the text file listing all videos
182
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
183
+ for p in video_paths:
184
+ # Format absolute path with forward slashes and escape single quotes
185
+ abs_path = os.path.abspath(p).replace('\\', '/')
186
+ safe_path = abs_path.replace("'", "'\\''")
187
+ f.write(f"file '{safe_path}'\n")
188
+ list_file_path = f.name
189
+
190
  temp_video_only = os.path.join(os.path.dirname(output_path), "temp_video_only.mp4")
 
191
 
192
  try:
193
+ # Step 1: Concatenate video tracks only (discard original segment audios to avoid jumps)
194
+ cmd_copy = [
195
+ 'ffmpeg', '-y',
196
+ '-f', 'concat',
197
+ '-safe', '0',
198
+ '-i', list_file_path,
199
+ '-an', # Discard segment audios completely
200
+ '-c:v', 'copy', # Direct stream copy (instant & lossless)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  temp_video_only
202
+ ]
203
+ print(f"Executing lossless FFMPEG video concat (no-audio): {' '.join(cmd_copy)}")
204
+ result = subprocess.run(cmd_copy, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
205
 
206
+ if result.returncode != 0:
207
+ print("Lossless video-only concatenation failed. Retrying with full video re-encoding...")
208
+ # Fallback path: video re-encoding (robust)
209
+ cmd_reencode = [
210
+ 'ffmpeg', '-y',
211
+ '-f', 'concat',
212
+ '-safe', '0',
213
+ '-i', list_file_path,
214
+ '-an',
215
+ '-c:v', 'libx264',
216
+ '-pix_fmt', 'yuv420p',
217
+ temp_video_only
218
+ ]
219
+ print(f"Executing FFMPEG video-only re-encode: {' '.join(cmd_reencode)}")
220
+ result_re = subprocess.run(cmd_reencode, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
221
+ if result_re.returncode != 0:
222
+ raise Exception(f"FFMPEG video concatenation failed: {result_re.stderr.decode('utf-8')}")
223
 
224
+ # Step 2: Merge the concatenated video track with the complete original audio file
225
  cmd_merge = [
226
  'ffmpeg', '-y',
227
  '-i', temp_video_only,
228
  '-i', str(full_audio_path),
229
+ '-map', '0:v:0', # Use video from concatenated file
230
+ '-map', '1:a:0', # Use audio from original full wav file
231
+ '-c:v', 'copy', # Keep video intact
232
  '-c:a', 'aac', # Encode original audio to standard clean AAC
233
+ '-shortest', # Sync to the shortest stream duration
234
  str(output_path)
235
  ]
236
  print(f"Applying final original audio track: {' '.join(cmd_merge)}")
 
239
  raise Exception(f"FFMPEG audio replacement failed: {result_merge.stderr.decode('utf-8')}")
240
 
241
  finally:
242
+ # Clean up temporary files
243
+ if os.path.exists(list_file_path):
244
+ os.remove(list_file_path)
245
  if os.path.exists(temp_video_only):
246
  try:
247
  os.remove(temp_video_only)
 
252
  # Create output directory if it doesn't exist
253
  os.makedirs(OUTPUT_DIR, exist_ok=True)
254
 
255
+ # Patch the LivePortrait config to enable full-frame pasteback
256
+ patch_liveportrait_config()
257
+
258
  # Download weights before initializing the pipeline
259
  download_weights()
260
 
 
388
  print("Falling back to the original source image for the next segment.")
389
  current_image_path = source_image_path
390
 
391
+ # 4. Merge all generated chunks and lay the original full audio track on top
392
  progress(0.95, desc="Merging segments and applying original audio...")
393
  final_video_name = f"final_{timestamp}.mp4"
394
  final_path = Path(os.path.join(run_output_dir, final_video_name))
395
 
396
+ # We always run this function (even for 1 chunk) to ensure the original, clean, high-quality WAV audio is perfectly mapped on the video
397
  concatenate_videos_ffmpeg(video_chunks_paths, wav_audio_path, final_path)
398
 
399
  except Exception as e: