omarbajouk commited on
Commit
ab9c83b
·
verified ·
1 Parent(s): 6f8b96c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -119
app.py CHANGED
@@ -1,10 +1,10 @@
 
1
  # ============================================================
2
- # CPAS Bruxelles — Créateur de Capsules (Gradio + Edge-TTS)
3
- # Version "Space HF" optimisée (imports différés)
4
- # Mode présentateur = VIDÉO DIRECTE (pas de SadTalker)
5
  # ============================================================
6
 
7
- import os, json, re, uuid, shutil, traceback, gc
8
  from typing import Optional
9
  import gradio as gr
10
  from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps
@@ -89,6 +89,13 @@ def _safe_name(stem, ext=".mp4"):
89
  import asyncio
90
  import edge_tts
91
  from pydub import AudioSegment
 
 
 
 
 
 
 
92
 
93
  EDGE_VOICES = {}
94
 
@@ -130,6 +137,8 @@ def get_edge_voices(lang="fr"):
130
  return [v for k, v in EDGE_VOICES.items() if k.startswith("nl-")]
131
  return list(EDGE_VOICES.values())
132
 
 
 
133
  async def _edge_tts_async(text, voice, outfile):
134
  communicate = edge_tts.Communicate(text, voice)
135
  await communicate.save(outfile)
@@ -173,6 +182,7 @@ def tts_gtts(text: str, lang: str = "fr") -> str:
173
 
174
  def _normalize_audio_to_wav(in_path: str) -> str:
175
  # Convertit n'importe quel format (mp3/wav) en WAV standard (44.1kHz stéréo)
 
176
  wav_path = os.path.join(TMP_DIR, f"norm_{uuid.uuid4().hex}.wav")
177
  snd = AudioSegment.from_file(in_path)
178
  snd = snd.set_frame_rate(44100).set_channels(2).set_sample_width(2)
@@ -248,6 +258,56 @@ def make_background(titre, sous_titre, texte_ecran, theme, logo_path, logo_pos,
248
  bg.save(out)
249
  return out
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  # ============================================================
252
  # SOUS-TITRES .SRT
253
  # ============================================================
@@ -302,114 +362,95 @@ def _write_video_with_fallback(final_clip, out_path_base, fps=25):
302
  raise RuntimeError(last_err or "FFmpeg a échoué")
303
 
304
  # ============================================================
305
- # BUILD CAPSULE — Pipeline vidéo direct
306
  # ============================================================
307
  def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
308
  image_fond=None, logo_path=None, logo_pos="haut-gauche",
309
  fond_mode="plein écran",
310
- video_presentateur=None,
311
- source_audio_option="Garder la voix originale de la vidéo",
312
  position_presentateur="bottom-right", plein=False,
313
- langue="fr", speaker=None):
314
 
315
- from moviepy.editor import ImageClip, AudioFileClip, CompositeVideoClip, VideoFileClip
316
- import moviepy.video.fx.all as vfx
317
-
318
- # 1) AUDIO PRINCIPAL
319
- audio_wav = None
320
- if video_presentateur and source_audio_option == "Garder la voix originale de la vidéo":
321
- vclip = VideoFileClip(video_presentateur)
322
- if vclip.audio is None:
323
- # Pas d'audio dans la vidéo => on force TTS
324
- print("[AUDIO] Vidéo sans audio, génération TTS.")
325
- try:
326
- audio_mp = tts_edge(texte_voix, voice=speaker or ("fr-FR-DeniseNeural" if langue == "fr" else "nl-NL-MaaikeNeural"))
327
- except Exception as e:
328
- print(f"[Capsule] Erreur TTS Edge ({e}), fallback gTTS.")
329
- audio_mp = tts_gtts(texte_voix, lang=langue)
330
- audio_wav = audio_mp if audio_mp.endswith(".wav") else _normalize_audio_to_wav(audio_mp)
331
- else:
332
- audio_wav = os.path.join(TMP_DIR, f"orig_{uuid.uuid4().hex}.wav")
333
- vclip.audio.write_audiofile(audio_wav, fps=44100, logger=None)
334
- vclip.close()
335
- else:
336
- # Remplacer par voix IA (Edge-TTS/gTTS)
337
  try:
338
- audio_mp = tts_edge(texte_voix, voice=speaker or ("fr-FR-DeniseNeural" if langue == "fr" else "nl-NL-MaaikeNeural"))
339
  except Exception as e:
340
- print(f"[Capsule] Erreur TTS Edge ({e}), fallback gTTS.")
341
- audio_mp = tts_gtts(texte_voix, lang=langue)
342
- audio_wav = audio_mp if audio_mp.endswith(".wav") else _normalize_audio_to_wav(audio_mp)
343
 
344
- audio = AudioFileClip(audio_wav)
345
- dur = float(audio.duration or 5.0)
346
- target_fps = 25
347
 
348
- # 2) FOND
349
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
350
  logo_path, logo_pos, image_fond, fond_mode)
351
 
352
- # 3) CLIPS
353
- bg = ImageClip(fond_path).set_duration(dur)
354
- clips = [bg]
355
 
356
- if video_presentateur and os.path.exists(video_presentateur):
357
- v = VideoFileClip(video_presentateur)
358
- if source_audio_option == "Remplacer par voix IA":
359
- v = v.without_audio()
360
- if plein:
361
- v = v.resize((W, H)).set_position(("center", "center"))
362
- else:
363
- v = v.resize(width=520)
364
- pos_map = {
365
- "bottom-right": ("right", "bottom"),
366
- "bottom-left": ("left", "bottom"),
367
- "top-right": ("right", "top"),
368
- "top-left": ("left", "top"),
369
- "center": ("center", "center"),
370
- }
371
- v = v.set_position(pos_map.get(position_presentateur, ("right", "bottom")))
372
- # Si la vidéo est plus courte, on boucle visuellement
373
- if v.duration and v.duration < dur:
374
- v = v.fx(vfx.loop, duration=dur)
375
- else:
376
- v = v.subclip(0, dur)
377
- clips.append(v)
378
 
379
- # 4) Composition + export
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
381
  name = _safe_name(f"{titre}_{langue}")
382
  out_base = os.path.join(OUT_DIR, name)
383
  out = _write_video_with_fallback(final, out_base, fps=target_fps)
384
 
385
- # 5) Sous-titres + manifest
386
- srt_path = None
387
- if texte_voix and texte_voix.strip():
388
- # Génère SRT basé sur le texte voix (utile si on remplace l'audio)
389
- # Si l'option garde voix originale est choisie, le SRT sera calé en durée
390
- srt_path = write_srt(texte_voix, dur)
391
-
392
  capsules.append({
393
  "file": out,
394
  "title": titre,
395
  "langue": langue,
396
- "voice": speaker or ("voix-originale" if source_audio_option.startswith("Garder") else "edge-tts"),
397
  "theme": theme,
398
  "duration": round(dur, 1)
399
  })
400
  _save_manifest()
401
 
402
- # 6) Nettoyage
403
  try:
404
  audio.close()
405
  final.close()
406
  bg.close()
407
- if os.path.exists(audio_wav): pass # conservé si besoin par MoviePy jusqu'à la fin
 
408
  except Exception as e:
409
  print(f"[Clean] Erreur nettoyage : {e}")
410
  gc.collect()
411
 
412
- return out, f"✅ Capsule {langue.upper()} créée ({dur:.1f}s, {('voix originale' if source_audio_option.startswith('Garder') else 'voix IA')})", srt_path
 
413
 
414
  # ============================================================
415
  # GESTION / ASSEMBLAGE
@@ -467,10 +508,11 @@ def deplacer_capsule(index, direction):
467
  # ============================================================
468
  print("[INIT] Lancement de Gradio...")
469
  init_edge_voices()
470
- with gr.Blocks(title="Créateur de Capsules CPAS – Vidéo directe",
471
  theme=gr.themes.Soft()) as demo:
472
 
473
- gr.Markdown("## 🎬 Créateur de Capsules CPAS Présentateur vidéo direct (sans SadTalker)")
 
474
 
475
  with gr.Tab("Créer une capsule"):
476
  with gr.Row():
@@ -481,57 +523,48 @@ with gr.Blocks(title="Créateur de Capsules CPAS – Vidéo directe",
481
  logo_path = gr.Image(label="🏛 Logo", type="filepath")
482
  logo_pos = gr.Radio(["haut-gauche","haut-droite","centre"],
483
  label="Position logo", value="haut-gauche")
484
-
485
- # 🎥 Vidéo du présentateur
486
- video_presentateur = gr.Video(label="🎥 Vidéo du présentateur (mp4, mov, mkv…)", type="filepath")
487
-
488
- # 🎧 Option audio
489
- source_audio_option = gr.Radio(
490
- ["Garder la voix originale de la vidéo", "Remplacer par voix IA"],
491
- label="Option audio", value="Garder la voix originale de la vidéo"
492
- )
493
-
494
  position_presentateur = gr.Radio(["bottom-right","bottom-left","top-right","top-left","center"],
495
- label="Position du présentateur", value="bottom-right")
496
  plein = gr.Checkbox(label="Plein écran présentateur", value=False)
497
-
498
  with gr.Column():
499
  titre = gr.Textbox(label="Titre", value="Aide médicale urgente / Dringende medische hulp")
500
  sous_titre = gr.Textbox(label="Sous-titre", value="Soins accessibles à tous / Toegankelijke zorg voor iedereen")
501
  theme = gr.Radio(list(THEMES.keys()), label="Thème", value="Bleu Professionnel")
502
-
503
- # Langue/voix pour Edge-TTS (si on remplace l'audio)
504
- langue = gr.Radio(["fr", "nl"], label="Langue de la voix IA", value="fr")
505
-
506
  def maj_voix(lang):
507
  try:
508
  voices = get_edge_voices(lang)
509
- # Valeur par défaut raisonnable
510
- default = "fr-FR-DeniseNeural" if (lang == "fr" and "fr-FR-DeniseNeural" in voices) else (voices[0] if voices else None)
511
- return gr.update(choices=voices, value=default)
512
- except Exception:
513
  return gr.update(choices=[], value=None)
514
-
 
 
515
  speaker_id = gr.Dropdown(
516
  label="🎙 Voix Edge-TTS",
517
  choices=get_edge_voices("fr"),
518
  value="fr-FR-DeniseNeural",
519
  info="Liste dynamique des voix Edge-TTS (FR & NL)"
520
  )
 
521
  langue.change(maj_voix, [langue], [speaker_id])
522
 
523
- texte_voix = gr.Textbox(
524
- label="Texte voix off (utilisé si 'Remplacer par voix IA')",
525
- lines=4,
526
- value="Bonjour, le CPAS de Bruxelles vous aide pour vos soins de santé."
 
527
  )
 
 
528
  texte_ecran = gr.Textbox(label="Texte à l'écran", lines=4,
529
  value="💊 Aides médicales\n🏥 Soins urgents\n📋 Formalités simplifiées")
530
-
531
  btn = gr.Button("🎬 Créer Capsule", variant="primary")
532
 
533
  sortie = gr.Video(label="Capsule générée")
534
- srt_out = gr.File(label="Sous-titres .srt (si texte fourni)")
535
  statut = gr.Markdown()
536
 
537
  with gr.Tab("Gestion & Assemblage"):
@@ -557,19 +590,12 @@ with gr.Blocks(title="Créateur de Capsules CPAS – Vidéo directe",
557
  sortie_finale = gr.Video(label="Vidéo finale")
558
  btn_asm.click(lambda: assemble_final(), [], [sortie_finale, message])
559
 
560
- # Callback création
561
- def creer_capsule_ui(t, st, tv, te, th,
562
- img, fmode, logo, pos_logo,
563
- vid_pres, src_audio_opt,
564
- pos_p, plein_opt, lang, speaker):
565
  try:
566
- vid, msg, srt = build_capsule(
567
- t, st, tv, te, th,
568
- image_fond=img, logo_path=logo, logo_pos=pos_logo, fond_mode=fmode,
569
- video_presentateur=vid_pres, source_audio_option=src_audio_opt,
570
- position_presentateur=pos_p, plein=plein_opt,
571
- langue=lang, speaker=speaker
572
- )
573
  return vid, srt, msg, table_capsules()
574
  except Exception as e:
575
  return None, None, f"❌ Erreur: {e}\n\n{traceback.format_exc()}", table_capsules()
@@ -578,11 +604,12 @@ with gr.Blocks(title="Créateur de Capsules CPAS – Vidéo directe",
578
  creer_capsule_ui,
579
  [titre, sous_titre, texte_voix, texte_ecran, theme,
580
  image_fond, fond_mode, logo_path, logo_pos,
581
- video_presentateur, source_audio_option,
582
- position_presentateur, plein,
583
- langue, speaker_id],
584
  [sortie, srt_out, statut, liste]
585
  )
586
 
 
 
587
  if __name__ == "__main__":
588
  demo.launch()
 
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
8
  from typing import Optional
9
  import gradio as gr
10
  from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps
 
89
  import asyncio
90
  import edge_tts
91
  from pydub import AudioSegment
92
+ import soundfile as sf
93
+
94
+ # ============================================================
95
+ # 🔊 CHARGEMENT DYNAMIQUE DES VOIX EDGE-TTS (FR/NL)
96
+ # ============================================================
97
+
98
+
99
 
100
  EDGE_VOICES = {}
101
 
 
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)
 
182
 
183
  def _normalize_audio_to_wav(in_path: str) -> str:
184
  # Convertit n'importe quel format (mp3/wav) en WAV standard (44.1kHz stéréo)
185
+ from pydub import AudioSegment
186
  wav_path = os.path.join(TMP_DIR, f"norm_{uuid.uuid4().hex}.wav")
187
  snd = AudioSegment.from_file(in_path)
188
  snd = snd.set_frame_rate(44100).set_channels(2).set_sample_width(2)
 
258
  bg.save(out)
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
  # ============================================================
312
  # SOUS-TITRES .SRT
313
  # ============================================================
 
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
 
374
+ # 1) TTS (Edge multivoix ou fallback)
375
+ try:
376
+ audio_mp = tts_edge(texte_voix, voice=speaker or ("fr-FR-DeniseNeural" if langue == "fr" else "nl-NL-MaaikeNeural"))
377
+ except Exception as e:
378
+ print(f"[Capsule] Erreur TTS Edge ({e}), fallback gTTS.")
379
+ audio_mp = tts_gtts(texte_voix, lang=langue)
380
+
381
+ # S'assurer qu'on a un WAV
382
+ audio_wav = audio_mp
383
+ if not audio_mp.lower().endswith(".wav"):
 
 
 
 
 
 
 
 
 
 
 
 
384
  try:
385
+ audio_wav = _normalize_audio_to_wav(audio_mp)
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)
393
 
394
+ # 3) MoviePy (imports lents ici seulement)
395
+ from moviepy.editor import ImageClip, AudioFileClip, CompositeVideoClip, VideoFileClip
396
+ import moviepy.video.fx.all as vfx
397
 
398
+ audio = AudioFileClip(audio_wav)
399
+ dur = float(audio.duration or 5.0)
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))
425
  name = _safe_name(f"{titre}_{langue}")
426
  out_base = os.path.join(OUT_DIR, name)
427
  out = _write_video_with_fallback(final, out_base, fps=target_fps)
428
 
429
+ # 6) Sous-titres + manifest
430
+ srt_path = write_srt(texte_voix, dur)
 
 
 
 
 
431
  capsules.append({
432
  "file": out,
433
  "title": titre,
434
  "langue": langue,
435
+ "voice": speaker or voix_type,
436
  "theme": theme,
437
  "duration": round(dur, 1)
438
  })
439
  _save_manifest()
440
 
441
+ # 7) Nettoyage
442
  try:
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:
449
  print(f"[Clean] Erreur nettoyage : {e}")
450
  gc.collect()
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
 
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
  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)
 
530
  with gr.Column():
531
  titre = gr.Textbox(label="Titre", value="Aide médicale urgente / Dringende medische hulp")
532
  sous_titre = gr.Textbox(label="Sous-titre", value="Soins accessibles à tous / Toegankelijke zorg voor iedereen")
533
  theme = gr.Radio(list(THEMES.keys()), label="Thème", value="Bleu Professionnel")
534
+ langue = gr.Radio(["fr", "nl"], label="Langue de la voix", value="fr")
535
+
 
 
536
  def maj_voix(lang):
537
  try:
538
  voices = get_edge_voices(lang)
539
+ return gr.update(choices=voices, value=voices[0] if voices else None)
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"),
548
  value="fr-FR-DeniseNeural",
549
  info="Liste dynamique des voix Edge-TTS (FR & NL)"
550
  )
551
+
552
  langue.change(maj_voix, [langue], [speaker_id])
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é.")
562
  texte_ecran = gr.Textbox(label="Texte à l'écran", lines=4,
563
  value="💊 Aides médicales\n🏥 Soins urgents\n📋 Formalités simplifiées")
 
564
  btn = gr.Button("🎬 Créer Capsule", variant="primary")
565
 
566
  sortie = gr.Video(label="Capsule générée")
567
+ srt_out = gr.File(label="Sous-titres .srt")
568
  statut = gr.Markdown()
569
 
570
  with gr.Tab("Gestion & Assemblage"):
 
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:
601
  return None, None, f"❌ Erreur: {e}\n\n{traceback.format_exc()}", table_capsules()
 
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()