omarbajouk commited on
Commit
84f0004
·
verified ·
1 Parent(s): ab9c83b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -85
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # app.py
2
  # ============================================================
3
  # CPAS Bruxelles — Créateur de Capsules (Gradio + Kokoro + SadTalker)
4
- # Version "Space HF" optimisée (chargement rapide, imports différés)
5
  # ============================================================
6
 
7
  import os, json, re, uuid, shutil, traceback, gc, subprocess
@@ -95,8 +95,6 @@ import soundfile as sf
95
  # 🔊 CHARGEMENT DYNAMIQUE DES VOIX EDGE-TTS (FR/NL)
96
  # ============================================================
97
 
98
-
99
-
100
  EDGE_VOICES = {}
101
 
102
  async def fetch_edge_voices_async():
@@ -137,8 +135,6 @@ def get_edge_voices(lang="fr"):
137
  return [v for k, v in EDGE_VOICES.items() if k.startswith("nl-")]
138
  return list(EDGE_VOICES.values())
139
 
140
-
141
-
142
  async def _edge_tts_async(text, voice, outfile):
143
  communicate = edge_tts.Communicate(text, voice)
144
  await communicate.save(outfile)
@@ -259,53 +255,42 @@ def make_background(titre, sous_titre, texte_ecran, theme, logo_path, logo_pos,
259
  return out
260
 
261
  # ============================================================
262
- # SadTalker appel subprocess (image -> visage animé)
263
  # ============================================================
264
- def _check_sadtalker_ready() -> Optional[str]:
265
- base = os.path.join(ROOT, "SadTalker")
266
- if not os.path.isdir(base):
267
- return "Dossier SadTalker manquant. Ajoutez 'SadTalker/' à la racine du Space (voir README)."
268
- ck = os.path.join(base, "checkpoints")
269
- needed = [
270
- "audio2exp.pt",
271
- "GFPGANv1.4.pth",
272
- "epoch_20.pth",
273
- "mapping_00229-model.pth.tar",
274
- "shape_predictor_68_face_landmarks.dat",
275
- ]
276
- missing = [f for f in needed if not os.path.exists(os.path.join(ck, f))]
277
- if missing:
278
- return "Checkpoints SadTalker manquants: " + ", ".join(missing)
279
- return None
280
-
281
- def generate_sadtalker_video(image_path, audio_path, output_dir=TMP_DIR, fps=25) -> Optional[str]:
282
- err = _check_sadtalker_ready()
283
- if err:
284
- # Pas d’échec brutal : on renvoie None (le fond seul sera utilisé)
285
- print(f"[SadTalker] {err}")
286
- return None
287
  try:
288
- os.makedirs(output_dir, exist_ok=True)
289
- out_path = os.path.join(output_dir, f"sadtalker_{uuid.uuid4().hex[:6]}.mp4")
290
- cmd = [
291
- "python", "inference.py",
292
- "--driven_audio", audio_path,
293
- "--source_image", image_path,
294
- "--result_dir", output_dir,
295
- "--still", "--enhancer", "gfpgan",
296
- "--fps", str(fps),
297
- ]
298
- subprocess.run(cmd, cwd=os.path.join(ROOT, "SadTalker"), check=True)
299
- # Récupérer le dernier mp4 créé
300
- candidates = [os.path.join(output_dir, f) for f in os.listdir(output_dir) if f.endswith(".mp4")]
301
- latest = max(candidates, key=os.path.getctime) if candidates else None
302
- if latest:
303
- # Harmoniser le nom
304
- shutil.move(latest, out_path)
305
- return out_path
306
- return None
 
 
 
 
 
 
307
  except Exception as e:
308
- print("[SadTalker] Erreur:", e)
309
  return None
310
 
311
  # ============================================================
@@ -362,12 +347,12 @@ def _write_video_with_fallback(final_clip, out_path_base, fps=25):
362
  raise RuntimeError(last_err or "FFmpeg a échoué")
363
 
364
  # ============================================================
365
- # BUILD CAPSULE — Pipeline complet (corrigé)
366
  # ============================================================
367
  def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
368
  image_fond=None, logo_path=None, logo_pos="haut-gauche",
369
  fond_mode="plein écran",
370
- image_presentateur=None, voix_type="Féminine",
371
  position_presentateur="bottom-right", plein=False,
372
  moteur_voix="Parler-TTS (offline)", langue="fr", speaker=None):
373
 
@@ -386,7 +371,6 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
386
  except Exception as e:
387
  print(f"[Audio] Normalisation échouée ({e}), on garde {audio_mp}")
388
 
389
-
390
  # 2) Fond (PIL)
391
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
392
  logo_path, logo_pos, image_fond, fond_mode)
@@ -400,25 +384,17 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
400
  target_fps = 25
401
  bg = ImageClip(fond_path).set_duration(dur)
402
 
403
- # 4) SadTalker (optionnel)
404
  clips = [bg]
405
- if image_presentateur and os.path.exists(image_presentateur):
406
- vpath = generate_sadtalker_video(image_presentateur, audio_wav, fps=target_fps)
407
- if vpath and os.path.exists(vpath):
408
- v = VideoFileClip(vpath).without_audio().fx(vfx.loop, duration=dur)
409
- if plein:
410
- v = v.resize((W, H)).set_position(("center", "center"))
411
- else:
412
- v = v.resize(width=520)
413
- pos_map = {
414
- "bottom-right": ("right", "bottom"),
415
- "bottom-left": ("left", "bottom"),
416
- "top-right": ("right", "top"),
417
- "top-left": ("left", "top"),
418
- "center": ("center", "center"),
419
- }
420
- v = v.set_position(pos_map.get(position_presentateur, ("right", "bottom")))
421
- clips.append(v)
422
 
423
  # 5) Composition + export
424
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
@@ -443,6 +419,8 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
443
  audio.close()
444
  final.close()
445
  bg.close()
 
 
446
  if os.path.exists(audio_mp): os.remove(audio_mp)
447
  if audio_wav != audio_mp and os.path.exists(audio_wav): os.remove(audio_wav)
448
  except Exception as e:
@@ -451,7 +429,6 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
451
 
452
  return out, f"✅ Capsule {langue.upper()} créée ({dur:.1f}s, voix {speaker or voix_type})", srt_path
453
 
454
-
455
  # ============================================================
456
  # GESTION / ASSEMBLAGE
457
  # ============================================================
@@ -508,11 +485,11 @@ def deplacer_capsule(index, direction):
508
  # ============================================================
509
  print("[INIT] Lancement de Gradio...")
510
  init_edge_voices()
511
- with gr.Blocks(title="Créateur de Capsules CPAS – SadTalker + Kokoro",
512
  theme=gr.themes.Soft()) as demo:
513
 
514
- gr.Markdown("## 🎬 Créateur de Capsules CPAS – Version complète (SadTalker + Kokoro)")
515
- gr.Markdown("**Astuce** : pour un démarrage instantané, chargez le dossier `SadTalker/checkpoints/` dans le Space (voir README).")
516
 
517
  with gr.Tab("Créer une capsule"):
518
  with gr.Row():
@@ -523,7 +500,8 @@ with gr.Blocks(title="Créateur de Capsules CPAS – SadTalker + Kokoro",
523
  logo_path = gr.Image(label="🏛 Logo", type="filepath")
524
  logo_pos = gr.Radio(["haut-gauche","haut-droite","centre"],
525
  label="Position logo", value="haut-gauche")
526
- image_presentateur = gr.Image(label="🧑‍🎨 Image du présentateur (portrait pour SadTalker)", type="filepath")
 
527
  position_presentateur = gr.Radio(["bottom-right","bottom-left","top-right","top-left","center"],
528
  label="Position", value="bottom-right")
529
  plein = gr.Checkbox(label="Plein écran présentateur", value=False)
@@ -540,8 +518,6 @@ with gr.Blocks(title="Créateur de Capsules CPAS – SadTalker + Kokoro",
540
  except Exception as e:
541
  return gr.update(choices=[], value=None)
542
 
543
-
544
-
545
  speaker_id = gr.Dropdown(
546
  label="🎙 Voix Edge-TTS",
547
  choices=get_edge_voices("fr"),
@@ -553,9 +529,9 @@ with gr.Blocks(title="Créateur de Capsules CPAS – SadTalker + Kokoro",
553
 
554
  voix_type = gr.Radio(["Féminine","Masculine"], label="Voix IA", value="Féminine")
555
  moteur_voix = gr.Radio(
556
- ["Kokoro (HuggingFace, offline)", "gTTS (en ligne)"],
557
  label="Moteur voix",
558
- value="Kokoro (HuggingFace, offline)"
559
  )
560
  texte_voix = gr.Textbox(label="Texte voix off", lines=4,
561
  value="Bonjour, le CPAS de Bruxelles vous aide pour vos soins de santé.")
@@ -590,11 +566,11 @@ with gr.Blocks(title="Créateur de Capsules CPAS – SadTalker + Kokoro",
590
  sortie_finale = gr.Video(label="Vidéo finale")
591
  btn_asm.click(lambda: assemble_final(), [], [sortie_finale, message])
592
 
593
- def creer_capsule_ui(t, st, tv, te, th, img, fmode, logo, pos_logo, ip, vx, pos_p, plein, motor, lang, speaker):
594
  try:
595
  vid, msg, srt = build_capsule(t, st, tv, te, th,
596
  img, logo, pos_logo, fmode,
597
- ip, vx, pos_p, plein,
598
  motor, lang, speaker=speaker)
599
  return vid, srt, msg, table_capsules()
600
  except Exception as e:
@@ -604,12 +580,10 @@ with gr.Blocks(title="Créateur de Capsules CPAS – SadTalker + Kokoro",
604
  creer_capsule_ui,
605
  [titre, sous_titre, texte_voix, texte_ecran, theme,
606
  image_fond, fond_mode, logo_path, logo_pos,
607
- image_presentateur, voix_type, position_presentateur,
608
  plein, moteur_voix, langue, speaker_id],
609
  [sortie, srt_out, statut, liste]
610
  )
611
 
612
-
613
-
614
  if __name__ == "__main__":
615
- demo.launch()
 
1
  # app.py
2
  # ============================================================
3
  # CPAS Bruxelles — Créateur de Capsules (Gradio + Kokoro + SadTalker)
4
+ # Version modifiée pour utiliser une vidéo de présentateur au lieu d'une image
5
  # ============================================================
6
 
7
  import os, json, re, uuid, shutil, traceback, gc, subprocess
 
95
  # 🔊 CHARGEMENT DYNAMIQUE DES VOIX EDGE-TTS (FR/NL)
96
  # ============================================================
97
 
 
 
98
  EDGE_VOICES = {}
99
 
100
  async def fetch_edge_voices_async():
 
135
  return [v for k, v in EDGE_VOICES.items() if k.startswith("nl-")]
136
  return list(EDGE_VOICES.values())
137
 
 
 
138
  async def _edge_tts_async(text, voice, outfile):
139
  communicate = edge_tts.Communicate(text, voice)
140
  await communicate.save(outfile)
 
255
  return out
256
 
257
  # ============================================================
258
+ # SUPPRESSION DE LA PARTIE SADTALKER (plus nécessaire)
259
  # ============================================================
260
+
261
+ def _prepare_video_presentateur(video_path, audio_duration, position, plein_ecran=False):
262
+ """Prépare la vidéo du présentateur avec la bonne durée et position."""
263
+ from moviepy.editor import VideoFileClip
264
+ import moviepy.video.fx.all as vfx
265
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  try:
267
+ v = VideoFileClip(video_path).without_audio()
268
+
269
+ # Ajuster la durée à celle de l'audio
270
+ if v.duration < audio_duration:
271
+ # Si la vidéo est plus courte, la boucler
272
+ v = v.fx(vfx.loop, duration=audio_duration)
273
+ elif v.duration > audio_duration:
274
+ # Si la vidéo est plus longue, la couper
275
+ v = v.subclip(0, audio_duration)
276
+
277
+ # Ajuster la taille et la position
278
+ if plein_ecran:
279
+ v = v.resize((W, H)).set_position(("center", "center"))
280
+ else:
281
+ v = v.resize(width=520) # Taille réduite pour le coin
282
+ pos_map = {
283
+ "bottom-right": ("right", "bottom"),
284
+ "bottom-left": ("left", "bottom"),
285
+ "top-right": ("right", "top"),
286
+ "top-left": ("left", "top"),
287
+ "center": ("center", "center"),
288
+ }
289
+ v = v.set_position(pos_map.get(position, ("right", "bottom")))
290
+
291
+ return v
292
  except Exception as e:
293
+ print(f"[Préparation vidéo] Erreur : {e}")
294
  return None
295
 
296
  # ============================================================
 
347
  raise RuntimeError(last_err or "FFmpeg a échoué")
348
 
349
  # ============================================================
350
+ # BUILD CAPSULE — Pipeline complet (modifié pour vidéo présentateur)
351
  # ============================================================
352
  def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
353
  image_fond=None, logo_path=None, logo_pos="haut-gauche",
354
  fond_mode="plein écran",
355
+ video_presentateur=None, voix_type="Féminine",
356
  position_presentateur="bottom-right", plein=False,
357
  moteur_voix="Parler-TTS (offline)", langue="fr", speaker=None):
358
 
 
371
  except Exception as e:
372
  print(f"[Audio] Normalisation échouée ({e}), on garde {audio_mp}")
373
 
 
374
  # 2) Fond (PIL)
375
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
376
  logo_path, logo_pos, image_fond, fond_mode)
 
384
  target_fps = 25
385
  bg = ImageClip(fond_path).set_duration(dur)
386
 
387
+ # 4) Vidéo présentateur (au lieu de SadTalker)
388
  clips = [bg]
389
+ if video_presentateur and os.path.exists(video_presentateur):
390
+ v_presentateur = _prepare_video_presentateur(
391
+ video_presentateur,
392
+ dur,
393
+ position_presentateur,
394
+ plein
395
+ )
396
+ if v_presentateur:
397
+ clips.append(v_presentateur)
 
 
 
 
 
 
 
 
398
 
399
  # 5) Composition + export
400
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
 
419
  audio.close()
420
  final.close()
421
  bg.close()
422
+ if 'v_presentateur' in locals():
423
+ v_presentateur.close()
424
  if os.path.exists(audio_mp): os.remove(audio_mp)
425
  if audio_wav != audio_mp and os.path.exists(audio_wav): os.remove(audio_wav)
426
  except Exception as e:
 
429
 
430
  return out, f"✅ Capsule {langue.upper()} créée ({dur:.1f}s, voix {speaker or voix_type})", srt_path
431
 
 
432
  # ============================================================
433
  # GESTION / ASSEMBLAGE
434
  # ============================================================
 
485
  # ============================================================
486
  print("[INIT] Lancement de Gradio...")
487
  init_edge_voices()
488
+ with gr.Blocks(title="Créateur de Capsules CPAS – Version avec vidéo présentateur",
489
  theme=gr.themes.Soft()) as demo:
490
 
491
+ gr.Markdown("## 🎬 Créateur de Capsules CPAS – Version avec vidéo présentateur")
492
+ gr.Markdown("**Nouveau** : Utilisez directement une vidéo de présentateur au lieu d'une image.")
493
 
494
  with gr.Tab("Créer une capsule"):
495
  with gr.Row():
 
500
  logo_path = gr.Image(label="🏛 Logo", type="filepath")
501
  logo_pos = gr.Radio(["haut-gauche","haut-droite","centre"],
502
  label="Position logo", value="haut-gauche")
503
+ # REMPLACEMENT : Image Video
504
+ video_presentateur = gr.Video(label="🎬 Vidéo du présentateur", type="filepath")
505
  position_presentateur = gr.Radio(["bottom-right","bottom-left","top-right","top-left","center"],
506
  label="Position", value="bottom-right")
507
  plein = gr.Checkbox(label="Plein écran présentateur", value=False)
 
518
  except Exception as e:
519
  return gr.update(choices=[], value=None)
520
 
 
 
521
  speaker_id = gr.Dropdown(
522
  label="🎙 Voix Edge-TTS",
523
  choices=get_edge_voices("fr"),
 
529
 
530
  voix_type = gr.Radio(["Féminine","Masculine"], label="Voix IA", value="Féminine")
531
  moteur_voix = gr.Radio(
532
+ ["Edge-TTS (recommandé)", "gTTS (fallback)"],
533
  label="Moteur voix",
534
+ value="Edge-TTS (recommandé)"
535
  )
536
  texte_voix = gr.Textbox(label="Texte voix off", lines=4,
537
  value="Bonjour, le CPAS de Bruxelles vous aide pour vos soins de santé.")
 
566
  sortie_finale = gr.Video(label="Vidéo finale")
567
  btn_asm.click(lambda: assemble_final(), [], [sortie_finale, message])
568
 
569
+ def creer_capsule_ui(t, st, tv, te, th, img, fmode, logo, pos_logo, vp, vx, pos_p, plein, motor, lang, speaker):
570
  try:
571
  vid, msg, srt = build_capsule(t, st, tv, te, th,
572
  img, logo, pos_logo, fmode,
573
+ vp, vx, pos_p, plein,
574
  motor, lang, speaker=speaker)
575
  return vid, srt, msg, table_capsules()
576
  except Exception as e:
 
580
  creer_capsule_ui,
581
  [titre, sous_titre, texte_voix, texte_ecran, theme,
582
  image_fond, fond_mode, logo_path, logo_pos,
583
+ video_presentateur, voix_type, position_presentateur, # Changé ici
584
  plein, moteur_voix, langue, speaker_id],
585
  [sortie, srt_out, statut, liste]
586
  )
587
 
 
 
588
  if __name__ == "__main__":
589
+ demo.launch()