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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +119 -146
app.py CHANGED
@@ -1,10 +1,10 @@
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,13 +89,6 @@ def _safe_name(stem, ext=".mp4"):
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,8 +130,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)
@@ -182,7 +173,6 @@ def tts_gtts(text: str, lang: str = "fr") -> str:
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,56 +248,6 @@ def make_background(titre, sous_titre, texte_ecran, theme, logo_path, logo_pos,
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,95 +302,114 @@ 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
 
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,11 +467,10 @@ 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,48 +481,57 @@ 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)
 
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,12 +557,19 @@ 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:
601
  return None, None, f"❌ Erreur: {e}\n\n{traceback.format_exc()}", table_capsules()
@@ -604,12 +578,11 @@ 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
  # ============================================================
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
  import asyncio
90
  import edge_tts
91
  from pydub import AudioSegment
 
 
 
 
 
 
 
92
 
93
  EDGE_VOICES = {}
94
 
 
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
 
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
  bg.save(out)
249
  return out
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  # ============================================================
252
  # SOUS-TITRES .SRT
253
  # ============================================================
 
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
  # ============================================================
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
  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
  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
  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()