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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +48 -60
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 (video track only) and overlays the original full audio track.
156
- This bypasses any pops or sync problems at the chunk boundaries.
 
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
- # Step 1: Concatenate video tracks only (discard original segment audios to avoid jumps)
171
- cmd_copy = [
172
- 'ffmpeg', '-y',
173
- '-f', 'concat',
174
- '-safe', '0',
175
- '-i', list_file_path,
176
- '-an', # Discard segment audios completely
177
- '-c:v', 'copy', # Direct stream copy (instant & lossless)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if result.returncode != 0:
184
- print("Lossless video-only concatenation failed. Retrying with full video re-encoding...")
185
- # Fallback path: video re-encoding (robust)
186
- cmd_reencode = [
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 concatenated video track with the complete original audio file
202
  cmd_merge = [
203
  'ffmpeg', '-y',
204
  '-i', temp_video_only,
205
  '-i', str(full_audio_path),
206
- '-map', '0:v:0', # Use video from concatenated file
207
- '-map', '1:a:0', # Use audio from original full wav file
208
- '-c:v', 'copy', # Keep video intact
209
  '-c:a', 'aac', # Encode original audio to standard clean AAC
210
- '-shortest', # Sync to the shortest stream duration
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 files
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 15 seconds, it automatically
255
- chunks the audio into 15-second blocks, processes them sequentially using the
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 - Increased to 15 seconds to avoid splitting common video lengths entirely
288
- CHUNK_SIZE_MS = 15000 # 15 seconds
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 15 seconds
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=True, # ENABLED: Uses Kalman filter to smooth face movements and prevent jumping
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 lay the original full audio track on top
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
- # We always run this function (even for 1 chunk) to ensure the original, clean, high-quality WAV audio is perfectly mapped on the video
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: