Rtop0726 commited on
Commit
1f024f3
Β·
verified Β·
1 Parent(s): ebf952a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +690 -654
app.py CHANGED
@@ -2,9 +2,9 @@ import os
2
  import cv2
3
  import math
4
  import shutil
5
- import subprocess
6
  import tempfile
7
  import traceback
 
8
  import urllib.request
9
 
10
  import gradio as gr
@@ -14,13 +14,12 @@ import onnxruntime as ort
14
 
15
 
16
  # ============================================================
17
- # CONFIG
18
  # ============================================================
19
 
20
  MODEL_PATH = "wav2lip_models/wav2lip_gan.onnx"
21
 
22
  OUTPUT_DIR = "outputs"
23
-
24
  FACE_DIR = "face_detector"
25
 
26
  FACE_PROTO = os.path.join(
@@ -33,13 +32,13 @@ FACE_MODEL = os.path.join(
33
  "res10_300x300_ssd_iter_140000.caffemodel"
34
  )
35
 
36
- PROTO_URL = (
37
  "https://raw.githubusercontent.com/"
38
  "opencv/opencv/master/samples/dnn/"
39
  "face_detector/deploy.prototxt"
40
  )
41
 
42
- MODEL_URL = (
43
  "https://github.com/opencv/opencv_3rdparty/"
44
  "raw/dnn_samples_face_detector_20170830/"
45
  "res10_300x300_ssd_iter_140000.caffemodel"
@@ -47,7 +46,7 @@ MODEL_URL = (
47
 
48
 
49
  # ============================================================
50
- # WAV2LIP AUDIO PARAMETERS
51
  # ============================================================
52
 
53
  SAMPLE_RATE = 16000
@@ -63,39 +62,52 @@ FMAX = 7600
63
 
64
  MEL_STEP_SIZE = 16
65
 
66
- # Original Wav2Lip uses approximately 80 mel frames/sec
67
  MEL_FPS = 80.0
68
 
69
 
70
  # ============================================================
71
- # VIDEO / FACE PARAMETERS
72
  # ============================================================
73
 
74
  FACE_SIZE = 96
75
 
76
  FACE_CONFIDENCE = 0.50
77
 
78
- # Extra face crop around detected face
79
- FACE_PADDING = 0.20
 
 
 
80
 
81
- # Temporal face smoothing
82
- FACE_SMOOTHING = 0.15
83
 
84
- # Mouth blend strength
85
- #
86
- # 0.70 = stronger lip replacement
87
- # 0.55 = balanced
88
- # 0.40 = more original face
89
- MOUTH_BLEND = 0.62
90
 
91
 
92
  # ============================================================
93
- # OUTPUT QUALITY
94
  # ============================================================
95
 
96
- FFMPEG_CRF = 17
 
 
 
 
 
 
 
 
 
97
 
98
- FFMPEG_PRESET = "medium"
 
 
 
 
 
99
 
100
 
101
  os.makedirs(
@@ -120,52 +132,56 @@ print("=" * 70)
120
  if not os.path.exists(MODEL_PATH):
121
 
122
  raise FileNotFoundError(
123
- "\nWav2Lip ONNX model not found:\n"
124
  f"{MODEL_PATH}\n\n"
125
- "Expected:\n"
126
- "wav2lip_models/wav2lip_gan.onnx"
 
127
  )
128
 
129
 
130
  # ============================================================
131
- # ONNX PROVIDERS
132
  # ============================================================
133
 
134
  available_providers = (
135
  ort.get_available_providers()
136
  )
137
 
 
 
 
 
 
 
138
  if "CUDAExecutionProvider" in available_providers:
139
 
140
- providers = [
141
  "CUDAExecutionProvider",
142
  "CPUExecutionProvider"
143
  ]
144
 
145
  else:
146
 
147
- providers = [
148
  "CPUExecutionProvider"
149
  ]
150
 
151
 
152
  print(
153
- "ONNX providers:",
154
- providers
155
  )
156
 
157
 
158
- # ============================================================
159
- # LOAD WAV2LIP
160
- # ============================================================
161
-
162
  print(
163
  "Loading Wav2Lip ONNX model..."
164
  )
165
 
 
166
  SESSION = ort.InferenceSession(
167
  MODEL_PATH,
168
- providers=providers
169
  )
170
 
171
 
@@ -203,10 +219,10 @@ for out in SESSION.get_outputs():
203
 
204
 
205
  # ============================================================
206
- # FACE DETECTOR
207
  # ============================================================
208
 
209
- def download_file(
210
  url,
211
  path
212
  ):
@@ -228,49 +244,53 @@ def download_file(
228
  )
229
 
230
 
231
- urllib.request.urlretrieve(
232
- url,
233
- path
234
- )
235
-
236
 
237
- def load_face_detector():
 
 
 
238
 
239
- download_file(
240
- PROTO_URL,
241
- FACE_PROTO
242
- )
243
 
244
- download_file(
245
- MODEL_URL,
246
- FACE_MODEL
247
- )
248
 
249
 
250
- print(
251
- "Loading OpenCV DNN face detector..."
252
- )
 
253
 
 
 
 
 
254
 
255
- detector = cv2.dnn.readNetFromCaffe(
256
- FACE_PROTO,
257
- FACE_MODEL
258
- )
259
 
 
 
 
260
 
261
- print(
262
- "Face detector loaded."
263
- )
264
 
265
 
266
- return detector
 
 
 
267
 
268
 
269
- FACE_DETECTOR = load_face_detector()
 
 
270
 
271
 
272
  # ============================================================
273
- # AUDIO LOADING
274
  # ============================================================
275
 
276
  def load_audio(
@@ -280,31 +300,31 @@ def load_audio(
280
  if not path:
281
 
282
  raise ValueError(
283
- "Audio file is required."
284
  )
285
 
286
 
287
- wav, sr = librosa.load(
288
  path,
289
  sr=SAMPLE_RATE,
290
  mono=True
291
  )
292
 
293
 
294
- if wav is None or len(wav) == 0:
295
 
296
  raise ValueError(
297
- "Could not read audio."
298
  )
299
 
300
 
301
- wav = wav.astype(
302
  np.float32
303
  )
304
 
305
 
306
  duration = (
307
- len(wav)
308
  / SAMPLE_RATE
309
  )
310
 
@@ -316,62 +336,45 @@ def load_audio(
316
  )
317
 
318
 
319
- return wav, duration
320
 
321
 
322
  # ============================================================
323
- # WAV2LIP-COMPATIBLE MEL SPECTROGRAM
324
  # ============================================================
325
 
326
- def wav2lip_mel(
327
- wav
328
  ):
329
 
330
  """
331
- Wav2Lip-style mel preprocessing.
 
 
 
332
 
333
- This is intentionally closer to the original Wav2Lip
334
- audio preprocessing than the previous implementation.
335
  """
336
 
337
 
338
- # Pre-emphasis / STFT
339
- stft = librosa.stft(
340
- wav,
341
  n_fft=N_FFT,
342
  hop_length=HOP_LENGTH,
343
  win_length=WIN_LENGTH,
344
  window="hann",
345
- center=True
346
- )
347
-
348
-
349
- # Magnitude
350
- magnitudes = np.abs(
351
- stft
352
- )
353
-
354
-
355
- # Mel filter bank
356
- mel_filters = librosa.filters.mel(
357
- sr=SAMPLE_RATE,
358
- n_fft=N_FFT,
359
  n_mels=N_MELS,
360
  fmin=FMIN,
361
- fmax=FMAX
362
- )
363
-
364
-
365
- mel = np.dot(
366
- mel_filters,
367
- magnitudes
368
  )
369
 
370
 
371
- # Wav2Lip-style log compression
372
  mel = np.maximum(
373
- 1e-5,
374
- mel
375
  )
376
 
377
 
@@ -389,13 +392,11 @@ def wav2lip_mel(
389
  # FACE DETECTION
390
  # ============================================================
391
 
392
- def detect_faces(
393
  frame
394
  ):
395
 
396
- height, width = (
397
- frame.shape[:2]
398
- )
399
 
400
 
401
  blob = cv2.dnn.blobFromImage(
@@ -406,9 +407,9 @@ def detect_faces(
406
  1.0,
407
  (300, 300),
408
  (
409
- 104.0,
410
- 177.0,
411
- 123.0
412
  )
413
  )
414
 
@@ -423,7 +424,8 @@ def detect_faces(
423
  )
424
 
425
 
426
- faces = []
 
427
 
428
 
429
  for i in range(
@@ -454,10 +456,10 @@ def detect_faces(
454
  ]
455
  * np.array(
456
  [
457
- width,
458
- height,
459
- width,
460
- height
461
  ]
462
  )
463
  )
@@ -472,7 +474,7 @@ def detect_faces(
472
  0,
473
  min(
474
  x1,
475
- width - 1
476
  )
477
  )
478
 
@@ -481,7 +483,7 @@ def detect_faces(
481
  0,
482
  min(
483
  y1,
484
- height - 1
485
  )
486
  )
487
 
@@ -490,7 +492,7 @@ def detect_faces(
490
  0,
491
  min(
492
  x2,
493
- width
494
  )
495
  )
496
 
@@ -499,164 +501,48 @@ def detect_faces(
499
  0,
500
  min(
501
  y2,
502
- height
503
  )
504
  )
505
 
506
 
507
- w = x2 - x1
508
- h = y2 - y1
509
 
510
 
511
- if w < 50 or h < 50:
512
 
513
  continue
514
 
515
 
516
- faces.append(
517
- {
518
- "x": x1,
519
- "y": y1,
520
- "w": w,
521
- "h": h,
522
- "confidence": confidence
523
- }
524
  )
525
 
526
 
527
- return faces
528
-
529
-
530
- # ============================================================
531
- # BEST FACE
532
- # ============================================================
533
-
534
- def best_face(
535
- frame
536
- ):
537
-
538
- faces = detect_faces(
539
- frame
540
- )
541
-
542
-
543
- if not faces:
544
-
545
- return None
546
-
547
-
548
- faces.sort(
549
- key=lambda f: (
550
- f["w"] * f["h"],
551
- f["confidence"]
552
- ),
553
- reverse=True
554
- )
555
-
556
-
557
- f = faces[0]
558
-
559
-
560
- return (
561
- f["x"],
562
- f["y"],
563
- f["w"],
564
- f["h"]
565
- )
566
-
567
-
568
- # ============================================================
569
- # FACE BOX WITH PADDING
570
- # ============================================================
571
-
572
- def padded_square_box(
573
- frame,
574
- box
575
- ):
576
-
577
- height, width = (
578
- frame.shape[:2]
579
- )
580
-
581
-
582
- x, y, w, h = box
583
-
584
-
585
- # Wav2Lip needs a little surrounding context
586
- size = max(
587
- w,
588
- h
589
- )
590
-
591
-
592
- size = int(
593
- size
594
- * (
595
- 1.0
596
- + FACE_PADDING
597
  )
598
- )
599
 
600
 
601
- size = min(
602
- size,
603
- width,
604
- height
605
- )
606
-
607
-
608
- cx = (
609
- x
610
- + w / 2.0
611
- )
612
 
 
613
 
614
- cy = (
615
- y
616
- + h / 2.0
617
- )
618
-
619
-
620
- x1 = int(
621
- cx
622
- - size / 2
623
- )
624
-
625
-
626
- y1 = int(
627
- cy
628
- - size / 2
629
- )
630
-
631
-
632
- x1 = max(
633
- 0,
634
- min(
635
- x1,
636
- width - size
637
- )
638
- )
639
-
640
-
641
- y1 = max(
642
- 0,
643
- min(
644
- y1,
645
- height - size
646
- )
647
- )
648
 
649
 
650
- return (
651
- int(x1),
652
- int(y1),
653
- int(size),
654
- int(size)
655
- )
656
 
657
 
658
  # ============================================================
659
- # INITIAL FACE BOX
660
  # ============================================================
661
 
662
  def find_initial_face(
@@ -671,7 +557,7 @@ def find_initial_face(
671
  if not cap.isOpened():
672
 
673
  raise RuntimeError(
674
- "Could not open video."
675
  )
676
 
677
 
@@ -687,12 +573,12 @@ def find_initial_face(
687
  cap.release()
688
 
689
  raise RuntimeError(
690
- "Video has no frames."
691
  )
692
 
693
 
694
- sample_count = min(
695
- 15,
696
  total
697
  )
698
 
@@ -701,19 +587,19 @@ def find_initial_face(
701
 
702
 
703
  for i in range(
704
- sample_count
705
  ):
706
 
707
- index = int(
708
  i
709
  * total
710
- / sample_count
711
  )
712
 
713
 
714
  cap.set(
715
  cv2.CAP_PROP_POS_FRAMES,
716
- index
717
  )
718
 
719
 
@@ -727,7 +613,7 @@ def find_initial_face(
727
  continue
728
 
729
 
730
- box = best_face(
731
  frame
732
  )
733
 
@@ -746,20 +632,20 @@ def find_initial_face(
746
 
747
  raise RuntimeError(
748
  "No face detected.\n\n"
749
- "Please use a video with a clearly "
750
- "visible face."
751
  )
752
 
753
 
754
- boxes = np.asarray(
755
  boxes,
756
  dtype=np.float32
757
  )
758
 
759
 
760
- # Median is more stable than taking the first frame
761
  median = np.median(
762
- boxes,
763
  axis=0
764
  )
765
 
@@ -770,27 +656,27 @@ def find_initial_face(
770
 
771
 
772
  # ============================================================
773
- # FACE BOX SMOOTHING
774
  # ============================================================
775
 
776
- def smooth_box(
777
- old_box,
778
- new_box
779
  ):
780
 
781
- if old_box is None:
782
 
783
- return new_box
784
 
785
 
786
  old = np.asarray(
787
- old_box,
788
  dtype=np.float32
789
  )
790
 
791
 
792
  new = np.asarray(
793
- new_box,
794
  dtype=np.float32
795
  )
796
 
@@ -813,253 +699,160 @@ def smooth_box(
813
 
814
 
815
  # ============================================================
816
- # MOUTH MASK
817
  # ============================================================
818
 
819
- def create_mouth_mask(
820
- size
 
821
  ):
822
 
823
- mask = np.zeros(
824
- (
825
- size,
826
- size
827
- ),
828
- dtype=np.float32
829
  )
830
 
831
 
832
- # Important:
833
- # Don't use a rectangular mouth region.
834
- #
835
- # An ellipse gives much smoother transitions and
836
- # avoids visible square/rectangular artifacts.
837
-
838
-
839
- center_x = int(
840
- size * 0.50
841
- )
842
 
843
 
844
- center_y = int(
845
- size * 0.665
 
 
 
 
 
846
  )
847
 
848
 
849
- axis_x = int(
850
- size * 0.285
 
 
 
851
  )
852
 
853
 
854
- axis_y = int(
855
- size * 0.155
 
 
856
  )
857
 
858
 
859
- cv2.ellipse(
860
- mask,
861
- (
862
- center_x,
863
- center_y
864
- ),
865
- (
866
- axis_x,
867
- axis_y
868
- ),
869
- 0,
870
- 0,
871
- 360,
872
- 1.0,
873
- -1
874
  )
875
 
876
 
877
- # Extra blur creates a natural transition
878
- sigma = max(
879
- 2,
880
- int(
881
- size * 0.045
882
- )
883
  )
884
 
885
 
886
- mask = cv2.GaussianBlur(
887
- mask,
888
- (0, 0),
889
- sigma
890
  )
891
 
892
 
893
- return np.clip(
894
- mask,
895
  0,
896
- 1
897
- )
898
-
899
-
900
- MOUTH_MASK = create_mouth_mask(
901
- FACE_SIZE
902
- )
903
-
904
-
905
- # ============================================================
906
- # MOUTH-ONLY COLOR CORRECTION
907
- # ============================================================
908
-
909
- def color_correct(
910
- generated,
911
- original,
912
- mask
913
- ):
914
-
915
- result = generated.astype(
916
- np.float32
917
- )
918
-
919
-
920
- original_float = (
921
- original.astype(
922
- np.float32
923
  )
924
  )
925
 
926
 
927
- strong = (
928
- mask > 0.35
929
- )
930
-
931
-
932
- if np.count_nonzero(
933
- strong
934
- ) < 50:
935
-
936
- return generated
937
-
938
-
939
- for c in range(3):
940
-
941
- src = (
942
- original_float[
943
- :,
944
- :,
945
- c
946
- ][strong]
947
- )
948
-
949
-
950
- dst = (
951
- result[
952
- :,
953
- :,
954
- c
955
- ][strong]
956
- )
957
-
958
-
959
- if (
960
- len(src) == 0
961
- or len(dst) == 0
962
- ):
963
-
964
- continue
965
-
966
-
967
- src_mean = np.mean(
968
- src
969
- )
970
-
971
-
972
- dst_mean = np.mean(
973
- dst
974
  )
 
975
 
976
 
977
- # Only mild correction.
978
- # Strong correction can make lips look unnatural.
979
- correction = (
980
- src_mean
981
- - dst_mean
982
- ) * 0.35
983
-
984
-
985
- result[
986
- :,
987
- :,
988
- c
989
- ] += correction
990
-
991
-
992
- return np.clip(
993
- result,
994
- 0,
995
- 255
996
- ).astype(
997
- np.uint8
998
  )
999
 
1000
 
1001
  # ============================================================
1002
- # WAV2LIP INFERENCE
1003
  # ============================================================
1004
 
1005
- def run_wav2lip(
1006
  face,
1007
- mel
1008
  ):
1009
 
1010
  # --------------------------------------------------------
1011
- # Resize to model input
1012
  # --------------------------------------------------------
1013
 
1014
- face_96 = cv2.resize(
1015
  face,
1016
  (
1017
- FACE_SIZE,
1018
- FACE_SIZE
1019
  ),
1020
  interpolation=cv2.INTER_AREA
1021
  )
1022
 
1023
 
1024
  # --------------------------------------------------------
1025
- # Mask lower half
 
 
 
1026
  # --------------------------------------------------------
1027
 
1028
- masked = face_96.copy()
1029
 
1030
 
1031
  masked[
1032
- FACE_SIZE // 2:
 
1033
  ] = 0
1034
 
1035
 
1036
  # --------------------------------------------------------
1037
  # 6 channels
1038
  #
1039
- # [masked face RGB]
1040
- # [original face RGB]
1041
  # --------------------------------------------------------
1042
 
1043
- combined = np.concatenate(
1044
  [
1045
  masked,
1046
- face_96
1047
  ],
1048
  axis=2
1049
  )
1050
 
1051
 
1052
- combined = (
1053
- combined.astype(
1054
- np.float32
1055
- )
 
 
 
1056
  / 255.0
1057
  )
1058
 
1059
 
1060
- # HWC -> CHW
1061
- combined = (
1062
- combined.transpose(
1063
  2,
1064
  0,
1065
  1
@@ -1067,8 +860,8 @@ def run_wav2lip(
1067
  )
1068
 
1069
 
1070
- video_input = (
1071
- combined[
1072
  np.newaxis,
1073
  ...
1074
  ]
@@ -1079,11 +872,11 @@ def run_wav2lip(
1079
 
1080
 
1081
  # --------------------------------------------------------
1082
- # Mel
1083
  # --------------------------------------------------------
1084
 
1085
  mel_input = (
1086
- mel[
1087
  np.newaxis,
1088
  np.newaxis,
1089
  :,
@@ -1099,23 +892,19 @@ def run_wav2lip(
1099
  # ONNX
1100
  # --------------------------------------------------------
1101
 
1102
- outputs = SESSION.run(
1103
  None,
1104
  {
1105
  "mel_spectrogram":
1106
  mel_input,
1107
 
1108
  "video_frames":
1109
- video_input
1110
  }
1111
  )
1112
 
1113
 
1114
- prediction = outputs[0]
1115
-
1116
-
1117
- # [1,3,96,96] -> [3,96,96]
1118
- prediction = prediction[0]
1119
 
1120
 
1121
  # --------------------------------------------------------
@@ -1132,43 +921,154 @@ def run_wav2lip(
1132
 
1133
 
1134
  prediction = np.clip(
1135
- prediction * 255.0,
1136
  0,
1137
- 255
 
 
 
 
 
 
1138
  ).astype(
1139
  np.uint8
1140
  )
1141
 
1142
 
1143
- # Model output is RGB
1144
  prediction = cv2.cvtColor(
1145
  prediction,
1146
  cv2.COLOR_RGB2BGR
1147
  )
1148
 
1149
 
1150
- # --------------------------------------------------------
1151
- # Back to original crop resolution
1152
- # --------------------------------------------------------
1153
 
1154
- prediction = cv2.resize(
1155
- prediction,
 
 
 
 
 
 
 
 
 
1156
  (
1157
- face.shape[1],
1158
- face.shape[0]
1159
  ),
1160
- interpolation=cv2.INTER_CUBIC
1161
  )
1162
 
1163
 
1164
- return prediction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
 
1166
 
1167
  # ============================================================
1168
- # BLEND RESULT
1169
  # ============================================================
1170
 
1171
- def blend_mouth(
1172
  original,
1173
  generated
1174
  ):
@@ -1188,43 +1088,114 @@ def blend_mouth(
1188
  )
1189
 
1190
 
1191
- mask = cv2.resize(
1192
- MOUTH_MASK,
1193
- (
1194
- w,
1195
- h
1196
- ),
1197
- interpolation=cv2.INTER_CUBIC
1198
  )
1199
 
1200
 
1201
- # Mild color correction
1202
- generated = color_correct(
1203
- generated,
1204
- original,
1205
- mask
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1206
  )
1207
 
1208
 
 
 
 
 
1209
  alpha = (
1210
  mask[
1211
  :,
1212
  :,
1213
  np.newaxis
1214
  ]
1215
- * MOUTH_BLEND
1216
  )
1217
 
1218
 
1219
- result = (
1220
- generated.astype(
1221
- np.float32
1222
- )
1223
  * alpha
1224
  +
1225
- original.astype(
1226
- np.float32
1227
- )
1228
  * (
1229
  1.0
1230
  - alpha
@@ -1233,7 +1204,7 @@ def blend_mouth(
1233
 
1234
 
1235
  return np.clip(
1236
- result,
1237
  0,
1238
  255
1239
  ).astype(
@@ -1245,34 +1216,34 @@ def blend_mouth(
1245
  # FFMPEG
1246
  # ============================================================
1247
 
1248
- def get_ffmpeg():
1249
 
1250
- ffmpeg = shutil.which(
1251
  "ffmpeg"
1252
  )
1253
 
1254
 
1255
- if ffmpeg is None:
1256
 
1257
  raise RuntimeError(
1258
  "FFmpeg is not installed."
1259
  )
1260
 
1261
 
1262
- return ffmpeg
1263
 
1264
 
1265
  # ============================================================
1266
  # MUX AUDIO
1267
  # ============================================================
1268
 
1269
- def mux_audio(
1270
- video,
1271
  audio,
1272
  output
1273
  ):
1274
 
1275
- ffmpeg = get_ffmpeg()
1276
 
1277
 
1278
  command = [
@@ -1280,7 +1251,7 @@ def mux_audio(
1280
  "-y",
1281
 
1282
  "-i",
1283
- video,
1284
 
1285
  "-i",
1286
  audio,
@@ -1295,10 +1266,10 @@ def mux_audio(
1295
  "libx264",
1296
 
1297
  "-preset",
1298
- FFMPEG_PRESET,
1299
 
1300
  "-crf",
1301
- str(FFMPEG_CRF),
1302
 
1303
  "-pix_fmt",
1304
  "yuv420p",
@@ -1334,32 +1305,79 @@ def mux_audio(
1334
 
1335
 
1336
  raise RuntimeError(
1337
- "FFmpeg audio muxing failed."
1338
  )
1339
 
1340
 
1341
  # ============================================================
1342
- # MAIN PROCESS
1343
  # ============================================================
1344
 
1345
  def process_video(
1346
- video_path,
1347
- audio_path,
1348
- text,
1349
  progress=gr.Progress()
1350
  ):
1351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1352
  if not video_path:
1353
 
1354
  raise gr.Error(
1355
- "Please upload a video."
1356
  )
1357
 
1358
 
1359
  if not audio_path:
1360
 
1361
  raise gr.Error(
1362
- "Please upload audio."
1363
  )
1364
 
1365
 
@@ -1374,9 +1392,9 @@ def process_video(
1374
  )
1375
 
1376
 
1377
- output_video = os.path.join(
1378
  OUTPUT_DIR,
1379
- "wav2lip_hq_result.mp4"
1380
  )
1381
 
1382
 
@@ -1388,11 +1406,11 @@ def process_video(
1388
 
1389
  progress(
1390
  0.02,
1391
- desc="Loading audio..."
1392
  )
1393
 
1394
 
1395
- wav, audio_duration = (
1396
  load_audio(
1397
  audio_path
1398
  )
@@ -1400,13 +1418,13 @@ def process_video(
1400
 
1401
 
1402
  progress(
1403
- 0.06,
1404
- desc="Creating Wav2Lip mel spectrogram..."
1405
  )
1406
 
1407
 
1408
- mel = wav2lip_mel(
1409
- wav
1410
  )
1411
 
1412
 
@@ -1416,13 +1434,21 @@ def process_video(
1416
  )
1417
 
1418
 
 
 
 
 
 
 
 
 
1419
  # ====================================================
1420
  # VIDEO
1421
  # ====================================================
1422
 
1423
  progress(
1424
  0.10,
1425
- desc="Opening video..."
1426
  )
1427
 
1428
 
@@ -1434,7 +1460,7 @@ def process_video(
1434
  if not cap.isOpened():
1435
 
1436
  raise RuntimeError(
1437
- "Could not open video."
1438
  )
1439
 
1440
 
@@ -1444,7 +1470,7 @@ def process_video(
1444
 
1445
 
1446
  if (
1447
- fps is None
1448
  or fps <= 0
1449
  ):
1450
 
@@ -1472,15 +1498,6 @@ def process_video(
1472
  )
1473
 
1474
 
1475
- if total_frames <= 0:
1476
-
1477
- cap.release()
1478
-
1479
- raise RuntimeError(
1480
- "Video contains no frames."
1481
- )
1482
-
1483
-
1484
  print(
1485
  "FPS:",
1486
  fps
@@ -1488,7 +1505,7 @@ def process_video(
1488
 
1489
 
1490
  print(
1491
- "Resolution:",
1492
  width,
1493
  "x",
1494
  height
@@ -1496,22 +1513,22 @@ def process_video(
1496
 
1497
 
1498
  print(
1499
- "Total frames:",
1500
  total_frames
1501
  )
1502
 
1503
 
1504
  # ====================================================
1505
- # FACE
1506
  # ====================================================
1507
 
1508
  progress(
1509
  0.14,
1510
- desc="Finding face..."
1511
  )
1512
 
1513
 
1514
- initial_face = (
1515
  find_initial_face(
1516
  video_path
1517
  )
@@ -1520,12 +1537,12 @@ def process_video(
1520
 
1521
  print(
1522
  "Initial face:",
1523
- initial_face
1524
  )
1525
 
1526
 
1527
  # ====================================================
1528
- # CREATE VIDEO WRITER
1529
  # ====================================================
1530
 
1531
  fourcc = (
@@ -1556,10 +1573,10 @@ def process_video(
1556
 
1557
 
1558
  # ====================================================
1559
- # PROCESS FRAME COUNT
1560
  # ====================================================
1561
 
1562
- audio_frame_count = int(
1563
  math.ceil(
1564
  audio_duration
1565
  * fps
@@ -1567,43 +1584,53 @@ def process_video(
1567
  )
1568
 
1569
 
1570
- process_frames = min(
1571
  total_frames,
1572
- audio_frame_count
1573
  )
1574
 
1575
 
1576
  print(
1577
  "Frames to process:",
1578
- process_frames
1579
  )
1580
 
1581
 
1582
  # ====================================================
1583
- # FACE TRACKING
1584
  # ====================================================
1585
 
1586
- current_face = initial_face
1587
-
1588
-
1589
- # How often to re-detect
1590
- #
1591
- # This prevents the face from drifting when the
1592
- # person moves.
1593
- REDETECT_EVERY = max(
1594
  1,
1595
  int(
1596
- fps * 0.5
 
1597
  )
1598
  )
1599
 
1600
 
1601
  # ====================================================
1602
- # PROCESS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  # ====================================================
1604
 
1605
  for frame_index in range(
1606
- process_frames
1607
  ):
1608
 
1609
  ok, frame = (
@@ -1617,15 +1644,16 @@ def process_video(
1617
 
1618
 
1619
  # ------------------------------------------------
1620
- # Re-detect periodically
1621
  # ------------------------------------------------
1622
 
1623
  if (
1624
- frame_index % REDETECT_EVERY
 
1625
  == 0
1626
  ):
1627
 
1628
- detected = best_face(
1629
  frame
1630
  )
1631
 
@@ -1633,7 +1661,7 @@ def process_video(
1633
  if detected is not None:
1634
 
1635
  current_face = (
1636
- smooth_box(
1637
  current_face,
1638
  detected
1639
  )
@@ -1641,27 +1669,29 @@ def process_video(
1641
 
1642
 
1643
  # ------------------------------------------------
1644
- # Face crop
1645
  # ------------------------------------------------
1646
 
1647
- crop_box = (
1648
- padded_square_box(
1649
- frame,
1650
- current_face
1651
- )
1652
  )
1653
 
1654
 
1655
- x, y, w, h = crop_box
1656
 
1657
 
1658
  face = frame[
1659
- y:y+h,
1660
- x:x+w
1661
  ]
1662
 
1663
 
1664
- if face.size == 0:
 
 
 
 
1665
 
1666
  writer.write(
1667
  frame
@@ -1671,23 +1701,8 @@ def process_video(
1671
 
1672
 
1673
  # ------------------------------------------------
1674
- # EXACT AUDIO/VIDEO FRAME MAPPING
1675
  # ------------------------------------------------
1676
- #
1677
- # This is more important for sync than a manually
1678
- # chosen arbitrary offset.
1679
- #
1680
- # Original Wav2Lip style:
1681
- #
1682
- # mel_idx_multiplier = 80 / fps
1683
- #
1684
- # ------------------------------------------------
1685
-
1686
- mel_idx_multiplier = (
1687
- MEL_FPS
1688
- / fps
1689
- )
1690
-
1691
 
1692
  mel_start = int(
1693
  frame_index
@@ -1702,7 +1717,7 @@ def process_video(
1702
 
1703
 
1704
  # ------------------------------------------------
1705
- # Handle final audio frames
1706
  # ------------------------------------------------
1707
 
1708
  if mel_start >= (
@@ -1729,7 +1744,7 @@ def process_video(
1729
 
1730
 
1731
  # ------------------------------------------------
1732
- # Pad final chunk
1733
  # ------------------------------------------------
1734
 
1735
  if (
@@ -1750,8 +1765,8 @@ def process_video(
1750
 
1751
  mel_chunk = np.zeros(
1752
  (
1753
- N_MELS,
1754
- MEL_STEP_SIZE
1755
  ),
1756
  dtype=np.float32
1757
  )
@@ -1774,33 +1789,48 @@ def process_video(
1774
  )
1775
 
1776
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1777
  # ------------------------------------------------
1778
- # Generate
1779
  # ------------------------------------------------
1780
 
1781
- generated = run_wav2lip(
1782
  face,
1783
  mel_chunk
1784
  )
1785
 
1786
 
1787
  # ------------------------------------------------
1788
- # Mouth-only blend
1789
  # ------------------------------------------------
1790
 
1791
- result_face = blend_mouth(
1792
- face,
1793
- generated
 
 
1794
  )
1795
 
1796
 
1797
  # ------------------------------------------------
1798
- # Paste back
1799
  # ------------------------------------------------
1800
 
1801
  frame[
1802
- y:y+h,
1803
- x:x+w
1804
  ] = result_face
1805
 
1806
 
@@ -1810,7 +1840,7 @@ def process_video(
1810
 
1811
 
1812
  # ------------------------------------------------
1813
- # Progress
1814
  # ------------------------------------------------
1815
 
1816
  if (
@@ -1818,22 +1848,26 @@ def process_video(
1818
  == 0
1819
  ):
1820
 
1821
- ratio = (
1822
  frame_index
1823
- / max(
 
1824
  1,
1825
- process_frames
1826
  )
1827
  )
1828
 
1829
 
1830
  progress(
1831
  0.18
1832
- + ratio * 0.70,
 
 
 
1833
  desc=(
1834
  "Lip syncing "
1835
  f"{frame_index + 1}/"
1836
- f"{process_frames}"
1837
  )
1838
  )
1839
 
@@ -1844,7 +1878,7 @@ def process_video(
1844
 
1845
 
1846
  # ====================================================
1847
- # AUDIO
1848
  # ====================================================
1849
 
1850
  progress(
@@ -1853,10 +1887,10 @@ def process_video(
1853
  )
1854
 
1855
 
1856
- mux_audio(
1857
  silent_video,
1858
  audio_path,
1859
- output_video
1860
  )
1861
 
1862
 
@@ -1866,46 +1900,47 @@ def process_video(
1866
 
1867
  progress(
1868
  1.0,
1869
- desc="Finished!"
1870
  )
1871
 
1872
 
1873
- file_size = (
1874
  os.path.getsize(
1875
- output_video
1876
- )
1877
- / (
1878
- 1024
1879
- * 1024
1880
  )
 
 
 
 
1881
  )
1882
 
1883
 
1884
  status = (
1885
  "## βœ… Lip-sync completed\n\n"
1886
- f"**Audio:** `{audio_duration:.2f}s`\n\n"
1887
- f"**FPS:** `{fps:.2f}`\n\n"
1888
- f"**Frames processed:** `{process_frames}`\n\n"
1889
- f"**Face tracking:** `Enabled`\n\n"
1890
- f"**Mouth-only blending:** `Enabled`\n\n"
1891
- f"**Output quality:** `CRF {FFMPEG_CRF}`\n\n"
1892
- f"**File size:** `{file_size:.2f} MB`"
 
1893
  )
1894
 
1895
 
1896
  if (
1897
- text
1898
- and text.strip()
1899
  ):
1900
 
1901
  status += (
1902
  "\n\n### πŸ“ Text reference\n\n"
1903
- + text.strip()
1904
  )
1905
 
1906
 
1907
  return (
1908
- output_video,
1909
  status
1910
  )
1911
 
@@ -1936,7 +1971,7 @@ def process_video(
1936
 
1937
 
1938
  # ============================================================
1939
- # GRADIO
1940
  # ============================================================
1941
 
1942
  CSS = """
@@ -1945,14 +1980,18 @@ CSS = """
1945
  margin: auto !important;
1946
  }
1947
 
1948
- #generate {
1949
- min-height: 58px;
1950
  font-size: 18px;
1951
  font-weight: 700;
1952
  }
1953
  """
1954
 
1955
 
 
 
 
 
1956
  with gr.Blocks(
1957
  title="Wav2Lip AI - High Quality Lip Sync"
1958
  ) as demo:
@@ -1961,16 +2000,20 @@ with gr.Blocks(
1961
  """
1962
  # 🎬 Wav2Lip AI β€” High Quality Lip Sync
1963
 
1964
- ### Upload a video + target audio β†’ generate synchronized speech
 
 
1965
  """
1966
  )
1967
 
1968
 
1969
  gr.Markdown(
1970
  """
1971
- **Improved version:** better Wav2Lip audio preprocessing,
1972
- more accurate frame-to-audio mapping, face tracking,
1973
- and smooth mouth-only blending to reduce mouth distortion.
 
 
1974
  """
1975
  )
1976
 
@@ -1997,7 +2040,7 @@ and smooth mouth-only blending to reduce mouth distortion.
1997
 
1998
 
1999
  text_input = gr.Textbox(
2000
- label="πŸ“ Audio Text (Optional)",
2001
  placeholder=(
2002
  "Example: Hello, welcome to my video."
2003
  ),
@@ -2006,9 +2049,9 @@ and smooth mouth-only blending to reduce mouth distortion.
2006
 
2007
 
2008
  generate_button = gr.Button(
2009
- "🎬 GENERATE HIGH-QUALITY LIP SYNC",
2010
  variant="primary",
2011
- elem_id="generate"
2012
  )
2013
 
2014
 
@@ -2019,44 +2062,37 @@ and smooth mouth-only blending to reduce mouth distortion.
2019
  with gr.Column():
2020
 
2021
  output_video = gr.Video(
2022
- label="🎬 Lip-Synced Result"
2023
  )
2024
 
2025
 
2026
- status_output = gr.Markdown(
2027
- label="Status"
2028
- )
2029
 
2030
 
2031
  # ========================================================
2032
- # TIPS
2033
  # ========================================================
2034
 
2035
  gr.Markdown(
2036
  """
2037
- ### πŸ’‘ For the best mouth quality
2038
-
2039
- - Use **one person** in the video.
2040
- - Face should be clearly visible.
2041
- - Front-facing or slightly angled faces work best.
2042
- - Avoid hands covering the mouth.
2043
- - Avoid very small faces.
2044
- - Use clean speech audio.
2045
- - Keep the video reasonably stable.
2046
- - The target audio should contain the speech you want the
2047
- person to perform.
2048
-
2049
- ### βš™οΈ Processing
2050
-
2051
- The system uses the uploaded **audio waveform**, not the
2052
- text field, to generate lip movement. The text box is only
2053
- provided as an optional reference.
2054
  """
2055
  )
2056
 
2057
 
2058
  # ========================================================
2059
- # BUTTON
2060
  # ========================================================
2061
 
2062
  generate_button.click(
@@ -2082,7 +2118,7 @@ provided as an optional reference.
2082
  if __name__ == "__main__":
2083
 
2084
  print("=" * 70)
2085
- print("Starting Wav2Lip High Quality Gradio Space")
2086
  print("=" * 70)
2087
 
2088
 
 
2
  import cv2
3
  import math
4
  import shutil
 
5
  import tempfile
6
  import traceback
7
+ import subprocess
8
  import urllib.request
9
 
10
  import gradio as gr
 
14
 
15
 
16
  # ============================================================
17
+ # CONFIGURATION
18
  # ============================================================
19
 
20
  MODEL_PATH = "wav2lip_models/wav2lip_gan.onnx"
21
 
22
  OUTPUT_DIR = "outputs"
 
23
  FACE_DIR = "face_detector"
24
 
25
  FACE_PROTO = os.path.join(
 
32
  "res10_300x300_ssd_iter_140000.caffemodel"
33
  )
34
 
35
+ FACE_PROTO_URL = (
36
  "https://raw.githubusercontent.com/"
37
  "opencv/opencv/master/samples/dnn/"
38
  "face_detector/deploy.prototxt"
39
  )
40
 
41
+ FACE_MODEL_URL = (
42
  "https://github.com/opencv/opencv_3rdparty/"
43
  "raw/dnn_samples_face_detector_20170830/"
44
  "res10_300x300_ssd_iter_140000.caffemodel"
 
46
 
47
 
48
  # ============================================================
49
+ # AUDIO
50
  # ============================================================
51
 
52
  SAMPLE_RATE = 16000
 
62
 
63
  MEL_STEP_SIZE = 16
64
 
65
+ # Wav2Lip audio timing
66
  MEL_FPS = 80.0
67
 
68
 
69
  # ============================================================
70
+ # FACE
71
  # ============================================================
72
 
73
  FACE_SIZE = 96
74
 
75
  FACE_CONFIDENCE = 0.50
76
 
77
+ # Do NOT make this too large.
78
+ #
79
+ # A large crop followed by 96x96 resizing causes
80
+ # the mouth to become soft/distorted.
81
+ FACE_PADDING = 0.08
82
 
83
+ # Face tracking smoothing
84
+ FACE_SMOOTHING = 0.25
85
 
86
+ # How frequently to re-detect face
87
+ REDETECT_SECONDS = 0.40
 
 
 
 
88
 
89
 
90
  # ============================================================
91
+ # BLENDING
92
  # ============================================================
93
 
94
+ # 1.0 = strongest generated mouth
95
+ # 0.7 = balanced
96
+ # 0.55 = preserve original texture
97
+ BLEND_STRENGTH = 0.72
98
+
99
+
100
+ # Lower-face mask starts around this percentage
101
+ # of the crop.
102
+ LOWER_FACE_START = 0.47
103
+
104
 
105
+ # ============================================================
106
+ # VIDEO ENCODING
107
+ # ============================================================
108
+
109
+ CRF = 17
110
+ PRESET = "medium"
111
 
112
 
113
  os.makedirs(
 
132
  if not os.path.exists(MODEL_PATH):
133
 
134
  raise FileNotFoundError(
135
+ "\n\nWav2Lip ONNX model not found:\n\n"
136
  f"{MODEL_PATH}\n\n"
137
+ "Your Space should contain:\n\n"
138
+ "wav2lip_models/\n"
139
+ " wav2lip_gan.onnx\n"
140
  )
141
 
142
 
143
  # ============================================================
144
+ # ONNX
145
  # ============================================================
146
 
147
  available_providers = (
148
  ort.get_available_providers()
149
  )
150
 
151
+ print(
152
+ "Available ONNX providers:",
153
+ available_providers
154
+ )
155
+
156
+
157
  if "CUDAExecutionProvider" in available_providers:
158
 
159
+ PROVIDERS = [
160
  "CUDAExecutionProvider",
161
  "CPUExecutionProvider"
162
  ]
163
 
164
  else:
165
 
166
+ PROVIDERS = [
167
  "CPUExecutionProvider"
168
  ]
169
 
170
 
171
  print(
172
+ "Using providers:",
173
+ PROVIDERS
174
  )
175
 
176
 
 
 
 
 
177
  print(
178
  "Loading Wav2Lip ONNX model..."
179
  )
180
 
181
+
182
  SESSION = ort.InferenceSession(
183
  MODEL_PATH,
184
+ providers=PROVIDERS
185
  )
186
 
187
 
 
219
 
220
 
221
  # ============================================================
222
+ # FACE DETECTOR DOWNLOAD
223
  # ============================================================
224
 
225
+ def download_if_needed(
226
  url,
227
  path
228
  ):
 
244
  )
245
 
246
 
247
+ try:
 
 
 
 
248
 
249
+ urllib.request.urlretrieve(
250
+ url,
251
+ path
252
+ )
253
 
254
+ except Exception as e:
 
 
 
255
 
256
+ raise RuntimeError(
257
+ f"Could not download {url}\n{e}"
258
+ )
 
259
 
260
 
261
+ download_if_needed(
262
+ FACE_PROTO_URL,
263
+ FACE_PROTO
264
+ )
265
 
266
+ download_if_needed(
267
+ FACE_MODEL_URL,
268
+ FACE_MODEL
269
+ )
270
 
 
 
 
 
271
 
272
+ # ============================================================
273
+ # FACE DETECTOR
274
+ # ============================================================
275
 
276
+ print(
277
+ "Loading OpenCV DNN face detector..."
278
+ )
279
 
280
 
281
+ FACE_DETECTOR = cv2.dnn.readNetFromCaffe(
282
+ FACE_PROTO,
283
+ FACE_MODEL
284
+ )
285
 
286
 
287
+ print(
288
+ "Face detector loaded."
289
+ )
290
 
291
 
292
  # ============================================================
293
+ # AUDIO
294
  # ============================================================
295
 
296
  def load_audio(
 
300
  if not path:
301
 
302
  raise ValueError(
303
+ "Target audio is required."
304
  )
305
 
306
 
307
+ audio, sr = librosa.load(
308
  path,
309
  sr=SAMPLE_RATE,
310
  mono=True
311
  )
312
 
313
 
314
+ if audio is None or len(audio) == 0:
315
 
316
  raise ValueError(
317
+ "Could not read target audio."
318
  )
319
 
320
 
321
+ audio = audio.astype(
322
  np.float32
323
  )
324
 
325
 
326
  duration = (
327
+ len(audio)
328
  / SAMPLE_RATE
329
  )
330
 
 
336
  )
337
 
338
 
339
+ return audio, duration
340
 
341
 
342
  # ============================================================
343
+ # WAV2LIP MEL
344
  # ============================================================
345
 
346
+ def make_mel(
347
+ audio
348
  ):
349
 
350
  """
351
+ Wav2Lip-style mel spectrogram.
352
+
353
+ Important:
354
+ The ONNX model expects exactly:
355
 
356
+ [1, 1, 80, 16]
 
357
  """
358
 
359
 
360
+ mel = librosa.feature.melspectrogram(
361
+ y=audio,
362
+ sr=SAMPLE_RATE,
363
  n_fft=N_FFT,
364
  hop_length=HOP_LENGTH,
365
  win_length=WIN_LENGTH,
366
  window="hann",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  n_mels=N_MELS,
368
  fmin=FMIN,
369
+ fmax=FMAX,
370
+ power=1.0,
371
+ center=True
 
 
 
 
372
  )
373
 
374
 
 
375
  mel = np.maximum(
376
+ mel,
377
+ 1e-5
378
  )
379
 
380
 
 
392
  # FACE DETECTION
393
  # ============================================================
394
 
395
+ def detect_face(
396
  frame
397
  ):
398
 
399
+ h, w = frame.shape[:2]
 
 
400
 
401
 
402
  blob = cv2.dnn.blobFromImage(
 
407
  1.0,
408
  (300, 300),
409
  (
410
+ 104,
411
+ 177,
412
+ 123
413
  )
414
  )
415
 
 
424
  )
425
 
426
 
427
+ best = None
428
+ best_score = 0.0
429
 
430
 
431
  for i in range(
 
456
  ]
457
  * np.array(
458
  [
459
+ w,
460
+ h,
461
+ w,
462
+ h
463
  ]
464
  )
465
  )
 
474
  0,
475
  min(
476
  x1,
477
+ w - 1
478
  )
479
  )
480
 
 
483
  0,
484
  min(
485
  y1,
486
+ h - 1
487
  )
488
  )
489
 
 
492
  0,
493
  min(
494
  x2,
495
+ w
496
  )
497
  )
498
 
 
501
  0,
502
  min(
503
  y2,
504
+ h
505
  )
506
  )
507
 
508
 
509
+ bw = x2 - x1
510
+ bh = y2 - y1
511
 
512
 
513
+ if bw < 40 or bh < 40:
514
 
515
  continue
516
 
517
 
518
+ area = (
519
+ bw * bh
 
 
 
 
 
 
520
  )
521
 
522
 
523
+ score = (
524
+ confidence
525
+ * math.sqrt(area)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  )
 
527
 
528
 
529
+ if score > best_score:
 
 
 
 
 
 
 
 
 
 
530
 
531
+ best_score = score
532
 
533
+ best = (
534
+ x1,
535
+ y1,
536
+ bw,
537
+ bh
538
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
 
540
 
541
+ return best
 
 
 
 
 
542
 
543
 
544
  # ============================================================
545
+ # FIND FACE FROM SEVERAL FRAMES
546
  # ============================================================
547
 
548
  def find_initial_face(
 
557
  if not cap.isOpened():
558
 
559
  raise RuntimeError(
560
+ "Could not open source video."
561
  )
562
 
563
 
 
573
  cap.release()
574
 
575
  raise RuntimeError(
576
+ "Source video contains no frames."
577
  )
578
 
579
 
580
+ samples = min(
581
+ 12,
582
  total
583
  )
584
 
 
587
 
588
 
589
  for i in range(
590
+ samples
591
  ):
592
 
593
+ frame_number = int(
594
  i
595
  * total
596
+ / samples
597
  )
598
 
599
 
600
  cap.set(
601
  cv2.CAP_PROP_POS_FRAMES,
602
+ frame_number
603
  )
604
 
605
 
 
613
  continue
614
 
615
 
616
+ box = detect_face(
617
  frame
618
  )
619
 
 
632
 
633
  raise RuntimeError(
634
  "No face detected.\n\n"
635
+ "Please use a video where the face is "
636
+ "clearly visible."
637
  )
638
 
639
 
640
+ arr = np.asarray(
641
  boxes,
642
  dtype=np.float32
643
  )
644
 
645
 
646
+ # Median is much more stable than first frame.
647
  median = np.median(
648
+ arr,
649
  axis=0
650
  )
651
 
 
656
 
657
 
658
  # ============================================================
659
+ # SMOOTH FACE BOX
660
  # ============================================================
661
 
662
+ def smooth_face(
663
+ old,
664
+ new
665
  ):
666
 
667
+ if old is None:
668
 
669
+ return new
670
 
671
 
672
  old = np.asarray(
673
+ old,
674
  dtype=np.float32
675
  )
676
 
677
 
678
  new = np.asarray(
679
+ new,
680
  dtype=np.float32
681
  )
682
 
 
699
 
700
 
701
  # ============================================================
702
+ # MAKE FACE CROP
703
  # ============================================================
704
 
705
+ def make_crop(
706
+ frame,
707
+ face_box
708
  ):
709
 
710
+ frame_h, frame_w = (
711
+ frame.shape[:2]
 
 
 
 
712
  )
713
 
714
 
715
+ x, y, w, h = face_box
 
 
 
 
 
 
 
 
 
716
 
717
 
718
+ # Keep crop close to actual face.
719
+ size = int(
720
+ max(w, h)
721
+ * (
722
+ 1.0
723
+ + FACE_PADDING
724
+ )
725
  )
726
 
727
 
728
+ # Prevent crop from becoming enormous.
729
+ size = min(
730
+ size,
731
+ frame_w,
732
+ frame_h
733
  )
734
 
735
 
736
+ # Face center
737
+ cx = (
738
+ x
739
+ + w / 2.0
740
  )
741
 
742
 
743
+ cy = (
744
+ y
745
+ + h / 2.0
 
 
 
 
 
 
 
 
 
 
 
 
746
  )
747
 
748
 
749
+ x1 = int(
750
+ cx
751
+ - size / 2
 
 
 
752
  )
753
 
754
 
755
+ y1 = int(
756
+ cy
757
+ - size / 2
 
758
  )
759
 
760
 
761
+ # Keep inside frame
762
+ x1 = max(
763
  0,
764
+ min(
765
+ x1,
766
+ frame_w - size
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
  )
768
  )
769
 
770
 
771
+ y1 = max(
772
+ 0,
773
+ min(
774
+ y1,
775
+ frame_h - size
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
776
  )
777
+ )
778
 
779
 
780
+ return (
781
+ int(x1),
782
+ int(y1),
783
+ int(size),
784
+ int(size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  )
786
 
787
 
788
  # ============================================================
789
+ # WAV2LIP MODEL
790
  # ============================================================
791
 
792
+ def inference(
793
  face,
794
+ mel_chunk
795
  ):
796
 
797
  # --------------------------------------------------------
798
+ # 96x96
799
  # --------------------------------------------------------
800
 
801
+ face96 = cv2.resize(
802
  face,
803
  (
804
+ 96,
805
+ 96
806
  ),
807
  interpolation=cv2.INTER_AREA
808
  )
809
 
810
 
811
  # --------------------------------------------------------
812
+ # ORIGINAL WAV2LIP MASKING
813
+ #
814
+ # Upper half remains original.
815
+ # Lower half is masked.
816
  # --------------------------------------------------------
817
 
818
+ masked = face96.copy()
819
 
820
 
821
  masked[
822
+ 48:,
823
+ :
824
  ] = 0
825
 
826
 
827
  # --------------------------------------------------------
828
  # 6 channels
829
  #
830
+ # [masked RGB]
831
+ # [original RGB]
832
  # --------------------------------------------------------
833
 
834
+ input_image = np.concatenate(
835
  [
836
  masked,
837
+ face96
838
  ],
839
  axis=2
840
  )
841
 
842
 
843
+ # --------------------------------------------------------
844
+ # HWC -> CHW
845
+ # --------------------------------------------------------
846
+
847
+ input_image = (
848
+ input_image
849
+ .astype(np.float32)
850
  / 255.0
851
  )
852
 
853
 
854
+ input_image = (
855
+ input_image.transpose(
 
856
  2,
857
  0,
858
  1
 
860
  )
861
 
862
 
863
+ input_image = (
864
+ input_image[
865
  np.newaxis,
866
  ...
867
  ]
 
872
 
873
 
874
  # --------------------------------------------------------
875
+ # AUDIO
876
  # --------------------------------------------------------
877
 
878
  mel_input = (
879
+ mel_chunk[
880
  np.newaxis,
881
  np.newaxis,
882
  :,
 
892
  # ONNX
893
  # --------------------------------------------------------
894
 
895
+ result = SESSION.run(
896
  None,
897
  {
898
  "mel_spectrogram":
899
  mel_input,
900
 
901
  "video_frames":
902
+ input_image
903
  }
904
  )
905
 
906
 
907
+ prediction = result[0][0]
 
 
 
 
908
 
909
 
910
  # --------------------------------------------------------
 
921
 
922
 
923
  prediction = np.clip(
924
+ prediction,
925
  0,
926
+ 1
927
+ )
928
+
929
+
930
+ prediction = (
931
+ prediction
932
+ * 255.0
933
  ).astype(
934
  np.uint8
935
  )
936
 
937
 
938
+ # Model returns RGB
939
  prediction = cv2.cvtColor(
940
  prediction,
941
  cv2.COLOR_RGB2BGR
942
  )
943
 
944
 
945
+ return prediction
 
 
946
 
947
+
948
+ # ============================================================
949
+ # SOFT LOWER FACE MASK
950
+ # ============================================================
951
+
952
+ def make_blend_mask(
953
+ h,
954
+ w
955
+ ):
956
+
957
+ mask = np.zeros(
958
  (
959
+ h,
960
+ w
961
  ),
962
+ dtype=np.float32
963
  )
964
 
965
 
966
+ start = int(
967
+ h
968
+ * LOWER_FACE_START
969
+ )
970
+
971
+
972
+ # Start from zero at upper edge.
973
+ # Gradually increase toward mouth.
974
+ #
975
+ # This avoids a visible rectangular patch.
976
+
977
+
978
+ for y in range(
979
+ start,
980
+ h
981
+ ):
982
+
983
+ position = (
984
+ y - start
985
+ ) / max(
986
+ 1,
987
+ h - start
988
+ )
989
+
990
+
991
+ # Smoothstep
992
+ value = (
993
+ position
994
+ * position
995
+ * (
996
+ 3
997
+ - 2 * position
998
+ )
999
+ )
1000
+
1001
+
1002
+ mask[y, :] = value
1003
+
1004
+
1005
+ # Stronger around mouth center
1006
+ yy, xx = np.mgrid[
1007
+ 0:h,
1008
+ 0:w
1009
+ ]
1010
+
1011
+
1012
+ cx = w * 0.50
1013
+ cy = h * 0.68
1014
+
1015
+ rx = w * 0.42
1016
+ ry = h * 0.30
1017
+
1018
+
1019
+ ellipse = (
1020
+ (
1021
+ (xx - cx) / rx
1022
+ ) ** 2
1023
+ +
1024
+ (
1025
+ (yy - cy) / ry
1026
+ ) ** 2
1027
+ )
1028
+
1029
+
1030
+ ellipse_mask = np.exp(
1031
+ -ellipse * 1.8
1032
+ )
1033
+
1034
+
1035
+ # Combine lower-face and mouth masks.
1036
+ mask = (
1037
+ mask
1038
+ * (
1039
+ 0.45
1040
+ + 0.55
1041
+ * ellipse_mask
1042
+ )
1043
+ )
1044
+
1045
+
1046
+ # Smooth edges.
1047
+ mask = cv2.GaussianBlur(
1048
+ mask,
1049
+ (
1050
+ 0,
1051
+ 0
1052
+ ),
1053
+ sigmaX=max(
1054
+ 1.0,
1055
+ w * 0.035
1056
+ )
1057
+ )
1058
+
1059
+
1060
+ return np.clip(
1061
+ mask,
1062
+ 0,
1063
+ 1
1064
+ )
1065
 
1066
 
1067
  # ============================================================
1068
+ # BLEND
1069
  # ============================================================
1070
 
1071
+ def blend_prediction(
1072
  original,
1073
  generated
1074
  ):
 
1088
  )
1089
 
1090
 
1091
+ mask = make_blend_mask(
1092
+ h,
1093
+ w
 
 
 
 
1094
  )
1095
 
1096
 
1097
+ # --------------------------------------------------------
1098
+ # Slight color matching.
1099
+ #
1100
+ # Do NOT aggressively change colors.
1101
+ # --------------------------------------------------------
1102
+
1103
+ original_float = (
1104
+ original.astype(
1105
+ np.float32
1106
+ )
1107
+ )
1108
+
1109
+
1110
+ generated_float = (
1111
+ generated.astype(
1112
+ np.float32
1113
+ )
1114
+ )
1115
+
1116
+
1117
+ mouth_area = (
1118
+ mask > 0.55
1119
+ )
1120
+
1121
+
1122
+ if np.count_nonzero(
1123
+ mouth_area
1124
+ ) > 100:
1125
+
1126
+ for c in range(3):
1127
+
1128
+ src = (
1129
+ original_float[
1130
+ :,
1131
+ :,
1132
+ c
1133
+ ][mouth_area]
1134
+ )
1135
+
1136
+
1137
+ gen = (
1138
+ generated_float[
1139
+ :,
1140
+ :,
1141
+ c
1142
+ ][mouth_area]
1143
+ )
1144
+
1145
+
1146
+ if (
1147
+ len(src) > 0
1148
+ and len(gen) > 0
1149
+ ):
1150
+
1151
+ src_mean = np.mean(
1152
+ src
1153
+ )
1154
+
1155
+ gen_mean = np.mean(
1156
+ gen
1157
+ )
1158
+
1159
+
1160
+ correction = (
1161
+ src_mean
1162
+ - gen_mean
1163
+ ) * 0.20
1164
+
1165
+
1166
+ generated_float[
1167
+ :,
1168
+ :,
1169
+ c
1170
+ ] += correction
1171
+
1172
+
1173
+ generated_float = np.clip(
1174
+ generated_float,
1175
+ 0,
1176
+ 255
1177
  )
1178
 
1179
 
1180
+ # --------------------------------------------------------
1181
+ # Blend
1182
+ # --------------------------------------------------------
1183
+
1184
  alpha = (
1185
  mask[
1186
  :,
1187
  :,
1188
  np.newaxis
1189
  ]
1190
+ * BLEND_STRENGTH
1191
  )
1192
 
1193
 
1194
+ output = (
1195
+ generated_float
 
 
1196
  * alpha
1197
  +
1198
+ original_float
 
 
1199
  * (
1200
  1.0
1201
  - alpha
 
1204
 
1205
 
1206
  return np.clip(
1207
+ output,
1208
  0,
1209
  255
1210
  ).astype(
 
1216
  # FFMPEG
1217
  # ============================================================
1218
 
1219
+ def ffmpeg_path():
1220
 
1221
+ path = shutil.which(
1222
  "ffmpeg"
1223
  )
1224
 
1225
 
1226
+ if path is None:
1227
 
1228
  raise RuntimeError(
1229
  "FFmpeg is not installed."
1230
  )
1231
 
1232
 
1233
+ return path
1234
 
1235
 
1236
  # ============================================================
1237
  # MUX AUDIO
1238
  # ============================================================
1239
 
1240
+ def add_audio(
1241
+ silent_video,
1242
  audio,
1243
  output
1244
  ):
1245
 
1246
+ ffmpeg = ffmpeg_path()
1247
 
1248
 
1249
  command = [
 
1251
  "-y",
1252
 
1253
  "-i",
1254
+ silent_video,
1255
 
1256
  "-i",
1257
  audio,
 
1266
  "libx264",
1267
 
1268
  "-preset",
1269
+ PRESET,
1270
 
1271
  "-crf",
1272
+ str(CRF),
1273
 
1274
  "-pix_fmt",
1275
  "yuv420p",
 
1305
 
1306
 
1307
  raise RuntimeError(
1308
+ "Could not add audio to output video."
1309
  )
1310
 
1311
 
1312
  # ============================================================
1313
+ # PROCESS
1314
  # ============================================================
1315
 
1316
  def process_video(
1317
+ video_input,
1318
+ audio_input,
1319
+ text_input,
1320
  progress=gr.Progress()
1321
  ):
1322
 
1323
+ # ========================================================
1324
+ # GET VIDEO PATH
1325
+ # ========================================================
1326
+
1327
+ video_path = video_input
1328
+
1329
+
1330
+ # Gradio can return a dictionary in some configurations.
1331
+ if isinstance(
1332
+ video_input,
1333
+ dict
1334
+ ):
1335
+
1336
+ video_path = (
1337
+ video_input.get(
1338
+ "path"
1339
+ )
1340
+ or
1341
+ video_input.get(
1342
+ "name"
1343
+ )
1344
+ )
1345
+
1346
+
1347
+ # ========================================================
1348
+ # AUDIO PATH
1349
+ # ========================================================
1350
+
1351
+ audio_path = audio_input
1352
+
1353
+
1354
+ if isinstance(
1355
+ audio_input,
1356
+ dict
1357
+ ):
1358
+
1359
+ audio_path = (
1360
+ audio_input.get(
1361
+ "path"
1362
+ )
1363
+ or
1364
+ audio_input.get(
1365
+ "name"
1366
+ )
1367
+ )
1368
+
1369
+
1370
  if not video_path:
1371
 
1372
  raise gr.Error(
1373
+ "Please upload a source video."
1374
  )
1375
 
1376
 
1377
  if not audio_path:
1378
 
1379
  raise gr.Error(
1380
+ "Please upload target audio."
1381
  )
1382
 
1383
 
 
1392
  )
1393
 
1394
 
1395
+ output_path = os.path.join(
1396
  OUTPUT_DIR,
1397
+ "wav2lip_result.mp4"
1398
  )
1399
 
1400
 
 
1406
 
1407
  progress(
1408
  0.02,
1409
+ desc="Loading target audio..."
1410
  )
1411
 
1412
 
1413
+ audio, audio_duration = (
1414
  load_audio(
1415
  audio_path
1416
  )
 
1418
 
1419
 
1420
  progress(
1421
+ 0.07,
1422
+ desc="Creating mel spectrogram..."
1423
  )
1424
 
1425
 
1426
+ mel = make_mel(
1427
+ audio
1428
  )
1429
 
1430
 
 
1434
  )
1435
 
1436
 
1437
+ if mel.shape[0] != 80:
1438
+
1439
+ raise RuntimeError(
1440
+ "Mel spectrogram has wrong number "
1441
+ f"of bands: {mel.shape[0]}"
1442
+ )
1443
+
1444
+
1445
  # ====================================================
1446
  # VIDEO
1447
  # ====================================================
1448
 
1449
  progress(
1450
  0.10,
1451
+ desc="Opening source video..."
1452
  )
1453
 
1454
 
 
1460
  if not cap.isOpened():
1461
 
1462
  raise RuntimeError(
1463
+ "Could not open source video."
1464
  )
1465
 
1466
 
 
1470
 
1471
 
1472
  if (
1473
+ not fps
1474
  or fps <= 0
1475
  ):
1476
 
 
1498
  )
1499
 
1500
 
 
 
 
 
 
 
 
 
 
1501
  print(
1502
  "FPS:",
1503
  fps
 
1505
 
1506
 
1507
  print(
1508
+ "Video:",
1509
  width,
1510
  "x",
1511
  height
 
1513
 
1514
 
1515
  print(
1516
+ "Frames:",
1517
  total_frames
1518
  )
1519
 
1520
 
1521
  # ====================================================
1522
+ # FIND FACE
1523
  # ====================================================
1524
 
1525
  progress(
1526
  0.14,
1527
+ desc="Locating face..."
1528
  )
1529
 
1530
 
1531
+ current_face = (
1532
  find_initial_face(
1533
  video_path
1534
  )
 
1537
 
1538
  print(
1539
  "Initial face:",
1540
+ current_face
1541
  )
1542
 
1543
 
1544
  # ====================================================
1545
+ # VIDEO WRITER
1546
  # ====================================================
1547
 
1548
  fourcc = (
 
1573
 
1574
 
1575
  # ====================================================
1576
+ # HOW MANY VIDEO FRAMES
1577
  # ====================================================
1578
 
1579
+ audio_frames = int(
1580
  math.ceil(
1581
  audio_duration
1582
  * fps
 
1584
  )
1585
 
1586
 
1587
+ frames_to_process = min(
1588
  total_frames,
1589
+ audio_frames
1590
  )
1591
 
1592
 
1593
  print(
1594
  "Frames to process:",
1595
+ frames_to_process
1596
  )
1597
 
1598
 
1599
  # ====================================================
1600
+ # FACE REDETECTION
1601
  # ====================================================
1602
 
1603
+ redetect_every = max(
 
 
 
 
 
 
 
1604
  1,
1605
  int(
1606
+ fps
1607
+ * REDETECT_SECONDS
1608
  )
1609
  )
1610
 
1611
 
1612
  # ====================================================
1613
+ # AUDIO MAPPING
1614
+ # ====================================================
1615
+
1616
+ mel_idx_multiplier = (
1617
+ MEL_FPS
1618
+ / fps
1619
+ )
1620
+
1621
+
1622
+ print(
1623
+ "Mel index multiplier:",
1624
+ mel_idx_multiplier
1625
+ )
1626
+
1627
+
1628
+ # ====================================================
1629
+ # FRAME LOOP
1630
  # ====================================================
1631
 
1632
  for frame_index in range(
1633
+ frames_to_process
1634
  ):
1635
 
1636
  ok, frame = (
 
1644
 
1645
 
1646
  # ------------------------------------------------
1647
+ # FACE TRACKING
1648
  # ------------------------------------------------
1649
 
1650
  if (
1651
+ frame_index
1652
+ % redetect_every
1653
  == 0
1654
  ):
1655
 
1656
+ detected = detect_face(
1657
  frame
1658
  )
1659
 
 
1661
  if detected is not None:
1662
 
1663
  current_face = (
1664
+ smooth_face(
1665
  current_face,
1666
  detected
1667
  )
 
1669
 
1670
 
1671
  # ------------------------------------------------
1672
+ # FACE CROP
1673
  # ------------------------------------------------
1674
 
1675
+ crop = make_crop(
1676
+ frame,
1677
+ current_face
 
 
1678
  )
1679
 
1680
 
1681
+ x, y, cw, ch = crop
1682
 
1683
 
1684
  face = frame[
1685
+ y:y+ch,
1686
+ x:x+cw
1687
  ]
1688
 
1689
 
1690
+ if (
1691
+ face.size == 0
1692
+ or face.shape[0] < 20
1693
+ or face.shape[1] < 20
1694
+ ):
1695
 
1696
  writer.write(
1697
  frame
 
1701
 
1702
 
1703
  # ------------------------------------------------
1704
+ # CORRECT MEL WINDOW
1705
  # ------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1706
 
1707
  mel_start = int(
1708
  frame_index
 
1717
 
1718
 
1719
  # ------------------------------------------------
1720
+ # End of audio
1721
  # ------------------------------------------------
1722
 
1723
  if mel_start >= (
 
1744
 
1745
 
1746
  # ------------------------------------------------
1747
+ # EXACTLY 16 MEL FRAMES
1748
  # ------------------------------------------------
1749
 
1750
  if (
 
1765
 
1766
  mel_chunk = np.zeros(
1767
  (
1768
+ 80,
1769
+ 16
1770
  ),
1771
  dtype=np.float32
1772
  )
 
1789
  )
1790
 
1791
 
1792
+ elif (
1793
+ mel_chunk.shape[1]
1794
+ > MEL_STEP_SIZE
1795
+ ):
1796
+
1797
+ mel_chunk = (
1798
+ mel_chunk[
1799
+ :,
1800
+ :MEL_STEP_SIZE
1801
+ ]
1802
+ )
1803
+
1804
+
1805
  # ------------------------------------------------
1806
+ # MODEL
1807
  # ------------------------------------------------
1808
 
1809
+ generated = inference(
1810
  face,
1811
  mel_chunk
1812
  )
1813
 
1814
 
1815
  # ------------------------------------------------
1816
+ # BLEND
1817
  # ------------------------------------------------
1818
 
1819
+ result_face = (
1820
+ blend_prediction(
1821
+ face,
1822
+ generated
1823
+ )
1824
  )
1825
 
1826
 
1827
  # ------------------------------------------------
1828
+ # PUT FACE BACK
1829
  # ------------------------------------------------
1830
 
1831
  frame[
1832
+ y:y+ch,
1833
+ x:x+cw
1834
  ] = result_face
1835
 
1836
 
 
1840
 
1841
 
1842
  # ------------------------------------------------
1843
+ # PROGRESS
1844
  # ------------------------------------------------
1845
 
1846
  if (
 
1848
  == 0
1849
  ):
1850
 
1851
+ fraction = (
1852
  frame_index
1853
+ /
1854
+ max(
1855
  1,
1856
+ frames_to_process
1857
  )
1858
  )
1859
 
1860
 
1861
  progress(
1862
  0.18
1863
+ +
1864
+ fraction
1865
+ * 0.70,
1866
+
1867
  desc=(
1868
  "Lip syncing "
1869
  f"{frame_index + 1}/"
1870
+ f"{frames_to_process}"
1871
  )
1872
  )
1873
 
 
1878
 
1879
 
1880
  # ====================================================
1881
+ # ADD AUDIO
1882
  # ====================================================
1883
 
1884
  progress(
 
1887
  )
1888
 
1889
 
1890
+ add_audio(
1891
  silent_video,
1892
  audio_path,
1893
+ output_path
1894
  )
1895
 
1896
 
 
1900
 
1901
  progress(
1902
  1.0,
1903
+ desc="Completed!"
1904
  )
1905
 
1906
 
1907
+ size_mb = (
1908
  os.path.getsize(
1909
+ output_path
 
 
 
 
1910
  )
1911
+ /
1912
+ 1024
1913
+ /
1914
+ 1024
1915
  )
1916
 
1917
 
1918
  status = (
1919
  "## βœ… Lip-sync completed\n\n"
1920
+ f"**Video FPS:** `{fps:.2f}`\n\n"
1921
+ f"**Audio duration:** `{audio_duration:.2f}s`\n\n"
1922
+ f"**Processed frames:** `{frames_to_process}`\n\n"
1923
+ "**Audio mapping:** `80 FPS mel mapping`\n\n"
1924
+ "**Face tracking:** `Enabled`\n\n"
1925
+ "**Soft mouth blending:** `Enabled`\n\n"
1926
+ f"**Video quality:** `CRF {CRF}`\n\n"
1927
+ f"**Output size:** `{size_mb:.2f} MB`"
1928
  )
1929
 
1930
 
1931
  if (
1932
+ text_input
1933
+ and text_input.strip()
1934
  ):
1935
 
1936
  status += (
1937
  "\n\n### πŸ“ Text reference\n\n"
1938
+ + text_input.strip()
1939
  )
1940
 
1941
 
1942
  return (
1943
+ output_path,
1944
  status
1945
  )
1946
 
 
1971
 
1972
 
1973
  # ============================================================
1974
+ # CSS
1975
  # ============================================================
1976
 
1977
  CSS = """
 
1980
  margin: auto !important;
1981
  }
1982
 
1983
+ #generate-button {
1984
+ min-height: 60px;
1985
  font-size: 18px;
1986
  font-weight: 700;
1987
  }
1988
  """
1989
 
1990
 
1991
+ # ============================================================
1992
+ # UI
1993
+ # ============================================================
1994
+
1995
  with gr.Blocks(
1996
  title="Wav2Lip AI - High Quality Lip Sync"
1997
  ) as demo:
 
2000
  """
2001
  # 🎬 Wav2Lip AI β€” High Quality Lip Sync
2002
 
2003
+ Upload a source video and target speech audio.
2004
+ The face will be tracked and the mouth synchronized
2005
+ to the target speech.
2006
  """
2007
  )
2008
 
2009
 
2010
  gr.Markdown(
2011
  """
2012
+ ### Improved pipeline
2013
+
2014
+ Correct Wav2Lip-style audio preprocessing β€’
2015
+ accurate audio/video mapping β€’ face tracking β€’
2016
+ soft mouth blending β€’ high-quality H.264 output
2017
  """
2018
  )
2019
 
 
2040
 
2041
 
2042
  text_input = gr.Textbox(
2043
+ label="πŸ“ Audio Text β€” Optional",
2044
  placeholder=(
2045
  "Example: Hello, welcome to my video."
2046
  ),
 
2049
 
2050
 
2051
  generate_button = gr.Button(
2052
+ "🎬 GENERATE LIP-SYNC VIDEO",
2053
  variant="primary",
2054
+ elem_id="generate-button"
2055
  )
2056
 
2057
 
 
2062
  with gr.Column():
2063
 
2064
  output_video = gr.Video(
2065
+ label="🎬 Lip-Synced Video"
2066
  )
2067
 
2068
 
2069
+ status_output = gr.Markdown()
 
 
2070
 
2071
 
2072
  # ========================================================
2073
+ # INFO
2074
  # ========================================================
2075
 
2076
  gr.Markdown(
2077
  """
2078
+ ### πŸ“Œ Best input
2079
+
2080
+ - One clearly visible face
2081
+ - Face looking toward the camera
2082
+ - Good lighting
2083
+ - Mouth not covered
2084
+ - 720p or higher source video recommended
2085
+ - Clean speech audio
2086
+ - Avoid very fast head movement
2087
+
2088
+ **Note:** The text box is only a reference. The actual
2089
+ lip movement is generated from the uploaded audio waveform.
 
 
 
 
 
2090
  """
2091
  )
2092
 
2093
 
2094
  # ========================================================
2095
+ # EVENT
2096
  # ========================================================
2097
 
2098
  generate_button.click(
 
2118
  if __name__ == "__main__":
2119
 
2120
  print("=" * 70)
2121
+ print("Starting Wav2Lip Gradio Space")
2122
  print("=" * 70)
2123
 
2124