omarbajouk commited on
Commit
ca0e393
·
verified ·
1 Parent(s): 392d3ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -93
app.py CHANGED
@@ -189,71 +189,125 @@ def _normalize_audio_to_wav(in_path: str) -> str:
189
  # FOND / GRAPHISME (PIL rapide)
190
  # ============================================================
191
  def make_background(titre, sous_titre, texte_ecran, theme, logo_path, logo_pos, img_fond, fond_mode="plein écran"):
192
- c = THEMES[theme]
193
- primary = tuple(c["primary"]); secondary = tuple(c["secondary"])
194
- bg = Image.new("RGB", (W, H), primary)
195
- if img_fond and os.path.exists(img_fond):
196
- img = Image.open(img_fond).convert("RGB")
197
- if fond_mode == "plein écran":
198
- img = img.resize((W, H))
199
- img = img.filter(ImageFilter.GaussianBlur(1))
200
- overlay = Image.new("RGBA", (W, H), (*primary, 90))
201
- bg = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
202
- elif fond_mode == "moitié gauche":
203
- img = img.resize((W//2, H))
204
- mask = Image.linear_gradient("L").resize((W//2, H))
205
- color = Image.new("RGB", (W//2, H), primary)
206
- comp = Image.composite(img, color, ImageOps.invert(mask))
207
- bg.paste(comp, (0, 0))
208
- elif fond_mode == "moitié droite":
209
- img = img.resize((W//2, H))
210
- mask = Image.linear_gradient("L").resize((W//2, H))
211
- color = Image.new("RGB", (W//2, H), primary)
212
- comp = Image.composite(color, img, mask)
213
- bg.paste(comp, (W//2, 0))
214
- elif fond_mode == "moitié bas":
215
- img = img.resize((W, H//2))
216
- mask = Image.linear_gradient("L").rotate(90).resize((W, H//2))
217
- color = Image.new("RGB", (W, H//2), primary)
218
- comp = Image.composite(color, img, mask)
219
- bg.paste(comp, (0, H//2))
220
-
221
- draw = ImageDraw.Draw(bg)
222
- f_title = ImageFont.truetype(FONT_BOLD, 84)
223
- f_sub = ImageFont.truetype(FONT_REG, 44)
224
- f_text = ImageFont.truetype(FONT_REG, 40)
225
- f_small = ImageFont.truetype(FONT_REG, 30)
226
-
227
- draw.rectangle([(0, 0), (W, 96)], fill=secondary)
228
- draw.rectangle([(0, H-96), (W, H)], fill=secondary)
229
-
230
- _draw_text_shadow(draw, (MARGIN_X, 30), "CPAS BRUXELLES • SERVICE PUBLIC", f_small)
231
- _draw_text_shadow(draw, (W//2-280, H-72), "📞 0800 35 550 • 🌐 cpasbru.irisnet.be", f_small)
232
- _draw_text_shadow(draw, (MARGIN_X, SAFE_Y_TOP), titre, f_title)
233
- _draw_text_shadow(draw, (MARGIN_X, SAFE_Y_TOP + 100), sous_titre, f_sub)
234
-
235
- y = SAFE_Y_TOP + 200
236
- for line in texte_ecran.split("\n"):
237
- for l in _wrap_text("• " + line.strip("• "), f_text, W - MARGIN_X*2, draw):
238
- _draw_text_shadow(draw, (MARGIN_X, y), l, f_text)
239
- y += 55
240
-
241
- # Cherchez cette partie dans make_background() et remplacez-la :
242
- if logo_path and os.path.exists(logo_path):
243
- logo = Image.open(logo_path).convert("RGBA")
244
- logo.thumbnail((260, 260))
245
- lw, lh = logo.size
246
- if logo_pos == "haut-gauche":
247
- pos = (50, 50)
248
- elif logo_pos == "haut-droite":
249
- pos = (W - lw - 50, 50)
250
- else:
251
- pos = ((W - lw)//2, 50)
252
- bg.paste(logo, pos, logo)
253
-
254
- out = os.path.join(TMP_DIR, f"fond_{uuid.uuid4().hex[:6]}.png")
255
- bg.save(out)
256
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  # ============================================================
259
  # SUPPRESSION DE LA PARTIE SADTALKER (plus nécessaire)
@@ -265,27 +319,29 @@ def _prepare_video_presentateur(video_path, audio_duration, position, plein_ecra
265
  import moviepy.video.fx.all as vfx
266
 
267
  try:
268
- print(f"[Préparation vidéo] Chargement: {video_path}")
 
 
 
 
269
  v = VideoFileClip(video_path).without_audio()
270
- print(f"[Préparation vidéo] Durée vidéo: {v.duration}s, Audio: {audio_duration}s")
271
 
272
  # Ajuster la durée à celle de l'audio
273
  if v.duration < audio_duration:
274
- # Si la vidéo est plus courte, la boucler
275
- print(f"[Préparation vidéo] Bouclage vidéo ({v.duration}s -> {audio_duration}s)")
276
  v = v.fx(vfx.loop, duration=audio_duration)
277
  elif v.duration > audio_duration:
278
- # Si la vidéo est plus longue, la couper
279
- print(f"[Préparation vidéo] Découpage vidéo ({v.duration}s -> {audio_duration}s)")
280
  v = v.subclip(0, audio_duration)
281
 
282
  # Ajuster la taille et la position
283
  if plein_ecran:
284
- print(f"[Préparation vidéo] Mode plein écran")
285
  v = v.resize((W, H)).set_position(("center", "center"))
286
  else:
287
- print(f"[Préparation vidéo] Mode incrustation, position: {position}")
288
- v = v.resize(width=520) # Taille réduite pour le coin
289
  pos_map = {
290
  "bottom-right": ("right", "bottom"),
291
  "bottom-left": ("left", "bottom"),
@@ -295,11 +351,12 @@ def _prepare_video_presentateur(video_path, audio_duration, position, plein_ecra
295
  }
296
  v = v.set_position(pos_map.get(position, ("right", "bottom")))
297
 
298
- print(f"[Préparation vidéo] Vidéo préparée avec succès")
299
  return v
 
300
  except Exception as e:
301
- print(f"[Préparation vidéo] Erreur : {e}")
302
- print(f"[Préparation vidéo] Traceback: {traceback.format_exc()}")
303
  return None
304
 
305
  # ============================================================
@@ -380,30 +437,49 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
380
  except Exception as e:
381
  print(f"[Audio] Normalisation échouée ({e}), on garde {audio_mp}")
382
 
383
-
384
- # 2) Fond (PIL)
385
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
386
  logo_path, logo_pos, image_fond, fond_mode)
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
  # 3) MoviePy (imports lents ici seulement)
389
  from moviepy.editor import ImageClip, AudioFileClip, CompositeVideoClip, VideoFileClip
 
390
  import moviepy.video.fx.all as vfx
391
 
392
  audio = AudioFileClip(audio_wav)
393
  dur = float(audio.duration or 5.0)
394
  target_fps = 25
395
 
396
- # CORRECTION : Vérifier que le fond existe
397
- if not os.path.exists(fond_path):
398
- print(f"❌ Fichier fond manquant, création d'urgence: {fond_path}")
399
- emergency_bg = Image.new("RGB", (W, H), (0, 82, 147))
400
- fond_path = os.path.join(TMP_DIR, f"emergency_{uuid.uuid4().hex[:6]}.png")
401
- emergency_bg.save(fond_path)
402
-
403
- bg = ImageClip(fond_path).set_duration(dur)
 
 
 
 
 
 
 
404
 
405
  # 4) Vidéo présentateur (au lieu de SadTalker)
406
- clips = [bg]
407
  if video_presentateur and os.path.exists(video_presentateur):
408
  print(f"[Capsule] Vidéo présentateur trouvée: {video_presentateur}")
409
  v_presentateur = _prepare_video_presentateur(
@@ -413,12 +489,12 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
413
  plein
414
  )
415
  if v_presentateur:
416
- print(f"[Capsule] Vidéo présentateur ajoutée avec succès")
417
  clips.append(v_presentateur)
418
  else:
419
- print(f"[Capsule] Échec de préparation de la vidéo présentateur")
420
  else:
421
- print(f"[Capsule] Aucune vidéo présentateur fournie ou fichier introuvable: {video_presentateur}")
422
 
423
  # 5) Composition + export
424
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
@@ -438,7 +514,7 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
438
  })
439
  _save_manifest()
440
 
441
- # 7) Nettoyage
442
  try:
443
  audio.close()
444
  final.close()
@@ -450,9 +526,10 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
450
  if audio_wav != audio_mp and os.path.exists(audio_wav):
451
  os.remove(audio_wav)
452
  except Exception as e:
453
- print(f"[Clean] Erreur nettoyage : {e}")
454
  gc.collect()
455
 
 
456
  # ============================================================
457
  # GESTION / ASSEMBLAGE
458
  # ============================================================
 
189
  # FOND / GRAPHISME (PIL rapide)
190
  # ============================================================
191
  def make_background(titre, sous_titre, texte_ecran, theme, logo_path, logo_pos, img_fond, fond_mode="plein écran"):
192
+ try:
193
+ print(f"[Fond] Création du fond - Thème: {theme}")
194
+
195
+ # Validation des entrées
196
+ if not titre:
197
+ titre = "Titre CPAS"
198
+ if not theme or theme not in THEMES:
199
+ theme = list(THEMES.keys())[0]
200
+ print(f"[Fond] Thème invalide, utilisation par défaut: {theme}")
201
+
202
+ c = THEMES[theme]
203
+ primary = tuple(c["primary"])
204
+ secondary = tuple(c["secondary"])
205
+
206
+ # Créer le fond de base
207
+ bg = Image.new("RGB", (W, H), primary)
208
+ draw = ImageDraw.Draw(bg)
209
+
210
+ # Application de l'image de fond si fournie
211
+ if img_fond and os.path.exists(img_fond):
212
+ try:
213
+ img = Image.open(img_fond).convert("RGB")
214
+ if fond_mode == "plein écran":
215
+ img = img.resize((W, H))
216
+ img = img.filter(ImageFilter.GaussianBlur(1))
217
+ overlay = Image.new("RGBA", (W, H), (*primary, 90))
218
+ bg = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
219
+ elif fond_mode == "moitié gauche":
220
+ img = img.resize((W//2, H))
221
+ mask = Image.linear_gradient("L").resize((W//2, H))
222
+ color = Image.new("RGB", (W//2, H), primary)
223
+ comp = Image.composite(img, color, ImageOps.invert(mask))
224
+ bg.paste(comp, (0, 0))
225
+ elif fond_mode == "moitié droite":
226
+ img = img.resize((W//2, H))
227
+ mask = Image.linear_gradient("L").resize((W//2, H))
228
+ color = Image.new("RGB", (W//2, H), primary)
229
+ comp = Image.composite(color, img, mask)
230
+ bg.paste(comp, (W//2, 0))
231
+ elif fond_mode == "moitié bas":
232
+ img = img.resize((W, H//2))
233
+ mask = Image.linear_gradient("L").rotate(90).resize((W, H//2))
234
+ color = Image.new("RGB", (W, H//2), primary)
235
+ comp = Image.composite(color, img, mask)
236
+ bg.paste(comp, (0, H//2))
237
+ draw = ImageDraw.Draw(bg) # Recréer le draw après modification
238
+ print(f"[Fond] Image de fond appliquée: {fond_mode}")
239
+ except Exception as e:
240
+ print(f"[Fond] Erreur image de fond: {e}")
241
+
242
+ # Chargement des polices avec fallback
243
+ try:
244
+ f_title = ImageFont.truetype(FONT_BOLD, 84)
245
+ f_sub = ImageFont.truetype(FONT_REG, 44)
246
+ f_text = ImageFont.truetype(FONT_REG, 40)
247
+ f_small = ImageFont.truetype(FONT_REG, 30)
248
+ except Exception as e:
249
+ print(f"[Fond] Erreur polices, utilisation par défaut: {e}")
250
+ # Polices par défaut
251
+ f_title = ImageFont.load_default()
252
+ f_sub = ImageFont.load_default()
253
+ f_text = ImageFont.load_default()
254
+ f_small = ImageFont.load_default()
255
+
256
+ # Bandes colorées
257
+ draw.rectangle([(0, 0), (W, 96)], fill=secondary)
258
+ draw.rectangle([(0, H-96), (W, H)], fill=secondary)
259
+
260
+ # Textes
261
+ _draw_text_shadow(draw, (MARGIN_X, 30), "CPAS BRUXELLES • SERVICE PUBLIC", f_small)
262
+ _draw_text_shadow(draw, (W//2-280, H-72), "📞 0800 35 550 • 🌐 cpasbru.irisnet.be", f_small)
263
+ _draw_text_shadow(draw, (MARGIN_X, SAFE_Y_TOP), titre, f_title)
264
+ _draw_text_shadow(draw, (MARGIN_X, SAFE_Y_TOP + 100), sous_titre, f_sub)
265
+
266
+ # Texte écran avec wrap
267
+ y = SAFE_Y_TOP + 200
268
+ if texte_ecran:
269
+ for line in texte_ecran.split("\n"):
270
+ wrapped_lines = _wrap_text("• " + line.strip("• "), f_text, W - MARGIN_X*2, draw)
271
+ for l in wrapped_lines:
272
+ _draw_text_shadow(draw, (MARGIN_X, y), l, f_text)
273
+ y += 55
274
+
275
+ # Logo
276
+ if logo_path and os.path.exists(logo_path):
277
+ try:
278
+ logo = Image.open(logo_path).convert("RGBA")
279
+ logo.thumbnail((260, 260))
280
+ lw, lh = logo.size
281
+ if logo_pos == "haut-gauche":
282
+ pos = (50, 50)
283
+ elif logo_pos == "haut-droite":
284
+ pos = (W - lw - 50, 50)
285
+ else: # centre
286
+ pos = ((W - lw)//2, 50)
287
+ bg.paste(logo, pos, logo)
288
+ print(f"[Fond] Logo appliqué: {logo_pos}")
289
+ except Exception as e:
290
+ print(f"[Fond] Erreur logo: {e}")
291
+
292
+ # Sauvegarde garantie
293
+ out_path = os.path.join(TMP_DIR, f"fond_{uuid.uuid4().hex[:6]}.png")
294
+ bg.save(out_path)
295
+ print(f"[Fond] ✅ Fond créé avec succès: {out_path}")
296
+ return out_path
297
+
298
+ except Exception as e:
299
+ print(f"[Fond] ❌ ERREUR CRITIQUE: {e}")
300
+ print(f"[Fond] Traceback: {traceback.format_exc()}")
301
+ # Fallback absolu
302
+ try:
303
+ emergency_bg = Image.new("RGB", (W, H), (0, 82, 147))
304
+ out_path = os.path.join(TMP_DIR, f"emergency_fond_{uuid.uuid4().hex[:6]}.png")
305
+ emergency_bg.save(out_path)
306
+ print(f"[Fond] ✅ Fond d'urgence créé: {out_path}")
307
+ return out_path
308
+ except Exception as e2:
309
+ print(f"[Fond] ❌ Même le fallback a échoué: {e2}")
310
+ return None
311
 
312
  # ============================================================
313
  # SUPPRESSION DE LA PARTIE SADTALKER (plus nécessaire)
 
319
  import moviepy.video.fx.all as vfx
320
 
321
  try:
322
+ print(f"[Video] Chargement: {video_path}")
323
+ if not os.path.exists(video_path):
324
+ print(f"[Video] ❌ Fichier introuvable: {video_path}")
325
+ return None
326
+
327
  v = VideoFileClip(video_path).without_audio()
328
+ print(f"[Video] Durée vidéo: {v.duration}s, Audio: {audio_duration}s")
329
 
330
  # Ajuster la durée à celle de l'audio
331
  if v.duration < audio_duration:
332
+ print(f"[Video] Bouclage nécessaire ({v.duration}s -> {audio_duration}s)")
 
333
  v = v.fx(vfx.loop, duration=audio_duration)
334
  elif v.duration > audio_duration:
335
+ print(f"[Video] Découpage nécessaire ({v.duration}s -> {audio_duration}s)")
 
336
  v = v.subclip(0, audio_duration)
337
 
338
  # Ajuster la taille et la position
339
  if plein_ecran:
340
+ print(f"[Video] Mode plein écran")
341
  v = v.resize((W, H)).set_position(("center", "center"))
342
  else:
343
+ print(f"[Video] Mode incrustation, position: {position}")
344
+ v = v.resize(width=520)
345
  pos_map = {
346
  "bottom-right": ("right", "bottom"),
347
  "bottom-left": ("left", "bottom"),
 
351
  }
352
  v = v.set_position(pos_map.get(position, ("right", "bottom")))
353
 
354
+ print(f"[Video] Vidéo préparée avec succès")
355
  return v
356
+
357
  except Exception as e:
358
+ print(f"[Video] Erreur préparation: {e}")
359
+ print(f"[Video] Traceback: {traceback.format_exc()}")
360
  return None
361
 
362
  # ============================================================
 
437
  except Exception as e:
438
  print(f"[Audio] Normalisation échouée ({e}), on garde {audio_mp}")
439
 
440
+ # 2) Fond (PIL) - SECTION CORRIGÉE
441
+ print(f"[Capsule] Génération du fond...")
442
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
443
  logo_path, logo_pos, image_fond, fond_mode)
444
+
445
+ # VÉRIFICATION ROBUSTE DU FOND
446
+ if fond_path is None:
447
+ print(f"[Capsule] ❌ fond_path est None, création d'urgence")
448
+ try:
449
+ emergency_bg = Image.new("RGB", (W, H), (0, 82, 147))
450
+ fond_path = os.path.join(TMP_DIR, f"emergency_{uuid.uuid4().hex[:6]}.png")
451
+ emergency_bg.save(fond_path)
452
+ print(f"[Capsule] ✅ Fond d'urgence créé: {fond_path}")
453
+ except Exception as e:
454
+ print(f"[Capsule] ❌ Impossible de créer le fond d'urgence: {e}")
455
+ fond_path = None
456
 
457
  # 3) MoviePy (imports lents ici seulement)
458
  from moviepy.editor import ImageClip, AudioFileClip, CompositeVideoClip, VideoFileClip
459
+ from moviepy.video.VideoClip import ColorClip
460
  import moviepy.video.fx.all as vfx
461
 
462
  audio = AudioFileClip(audio_wav)
463
  dur = float(audio.duration or 5.0)
464
  target_fps = 25
465
 
466
+ # CHARGEMENT ROBUSTE DU FOND
467
+ clips = []
468
+ if fond_path and os.path.exists(fond_path):
469
+ try:
470
+ bg = ImageClip(fond_path).set_duration(dur)
471
+ clips.append(bg)
472
+ print(f"[Capsule] ✅ Fond chargé: {fond_path}")
473
+ except Exception as e:
474
+ print(f"[Capsule] ❌ Erreur ImageClip, fallback ColorClip: {e}")
475
+ bg = ColorClip(size=(W, H), color=(0, 82, 147)).set_duration(dur)
476
+ clips.append(bg)
477
+ else:
478
+ print(f"[Capsule] ❌ Aucun fond valide, utilisation ColorClip")
479
+ bg = ColorClip(size=(W, H), color=(0, 82, 147)).set_duration(dur)
480
+ clips.append(bg)
481
 
482
  # 4) Vidéo présentateur (au lieu de SadTalker)
 
483
  if video_presentateur and os.path.exists(video_presentateur):
484
  print(f"[Capsule] Vidéo présentateur trouvée: {video_presentateur}")
485
  v_presentateur = _prepare_video_presentateur(
 
489
  plein
490
  )
491
  if v_presentateur:
492
+ print(f"[Capsule] Vidéo présentateur ajoutée")
493
  clips.append(v_presentateur)
494
  else:
495
+ print(f"[Capsule] Échec préparation vidéo présentateur")
496
  else:
497
+ print(f"[Capsule] Aucune vidéo présentateur: {video_presentateur}")
498
 
499
  # 5) Composition + export
500
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
 
514
  })
515
  _save_manifest()
516
 
517
+ # 7) Nettoyage CORRIGÉ
518
  try:
519
  audio.close()
520
  final.close()
 
526
  if audio_wav != audio_mp and os.path.exists(audio_wav):
527
  os.remove(audio_wav)
528
  except Exception as e:
529
+ print(f"[Clean] Erreur nettoyage: {e}")
530
  gc.collect()
531
 
532
+ return out, f"✅ Capsule {langue.upper()} créée ({dur:.1f}s, voix {speaker or voix_type})", srt_path
533
  # ============================================================
534
  # GESTION / ASSEMBLAGE
535
  # ============================================================