omarbajouk commited on
Commit
75070d3
·
verified ·
1 Parent(s): 26908ae

Update src/capsule_builder.py

Browse files
Files changed (1) hide show
  1. src/capsule_builder.py +59 -53
src/capsule_builder.py CHANGED
@@ -1,5 +1,5 @@
1
- import os, json, uuid, gc, traceback
2
- from moviepy.editor import ImageClip, AudioFileClip, CompositeVideoClip
3
  from moviepy.video.VideoClip import ColorClip
4
 
5
  from .config import OUT_DIR, TMP_DIR, MANIFEST_PATH, W, H
@@ -9,7 +9,7 @@ from .video_utils import prepare_video_presentateur, write_srt, write_video_with
9
 
10
  capsules = []
11
 
12
- # Load manifest on import
13
  if os.path.exists(MANIFEST_PATH):
14
  try:
15
  data = json.load(open(MANIFEST_PATH, "r", encoding="utf-8"))
@@ -18,18 +18,20 @@ if os.path.exists(MANIFEST_PATH):
18
  except Exception:
19
  pass
20
 
 
21
  def _save_manifest():
22
  with open(MANIFEST_PATH, "w", encoding="utf-8") as f:
23
  json.dump({"capsules": capsules}, f, ensure_ascii=False, indent=2)
24
 
 
25
  def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
26
  image_fond=None, logo_path=None, logo_pos="haut-gauche",
27
- fond_mode="plein écran",
28
- video_presentateur=None, voix_type="Féminine",
29
- position_presentateur="bottom-right", plein=False,
30
- moteur_voix="Edge-TTS (recommandé)", langue="fr", speaker=None):
31
 
32
- # 1) TTS
33
  try:
34
  audio_mp = tts_edge(texte_voix, voice=speaker or ("fr-FR-DeniseNeural" if langue == "fr" else "nl-NL-MaaikeNeural"))
35
  except Exception as e:
@@ -43,10 +45,18 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
43
  except Exception as e:
44
  print(f"[Audio] Normalisation échouée ({e}), on garde {audio_mp}")
45
 
46
- # 2) Fond
47
  print("[Capsule] Génération du fond...")
48
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
49
  logo_path, logo_pos, image_fond, fond_mode)
 
 
 
 
 
 
 
 
50
  if fond_path is None:
51
  print("[Capsule] ❌ fond_path est None, création d'urgence")
52
  from PIL import Image
@@ -59,7 +69,7 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
59
  print(f"[Capsule] ❌ Impossible de créer le fond d'urgence: {e}")
60
  fond_path = None
61
 
62
- # 3) Composition MoviePy
63
  audio = AudioFileClip(audio_wav)
64
  dur = float(audio.duration or 5.0)
65
  target_fps = 25
@@ -79,26 +89,24 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
79
  bg = ColorClip(size=(W, H), color=(0, 82, 147)).set_duration(dur)
80
  clips.append(bg)
81
 
 
 
 
 
 
82
  if video_presentateur and os.path.exists(video_presentateur):
83
  ext = os.path.splitext(video_presentateur)[1].lower()
84
- v_presentateur = None
85
-
86
- # --- Vidéo ---
87
  if ext in [".mp4", ".mov", ".avi", ".mkv"]:
88
  print(f"[Capsule] Vidéo présentateur trouvée: {video_presentateur}")
89
  v_presentateur = prepare_video_presentateur(
90
- video_presentateur, dur, position_presentateur, plein
91
  )
92
-
93
- # --- Image ---
94
  elif ext in [".jpg", ".jpeg", ".png", ".bmp", ".webp"]:
95
- from moviepy.editor import ImageClip
96
  print(f"[Capsule] Image présentateur trouvée: {video_presentateur}")
97
  try:
98
  img_clip = ImageClip(video_presentateur).set_duration(dur)
99
- if plein:
100
  img_clip = img_clip.resize((W, H)).set_position(("center", "center"))
101
- print("[Capsule] Image plein écran")
102
  else:
103
  img_clip = img_clip.resize(width=520)
104
  pos_map = {
@@ -109,21 +117,33 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
109
  "center": ("center", "center"),
110
  }
111
  img_clip = img_clip.set_position(pos_map.get(position_presentateur, ("right", "bottom")))
112
- print(f"[Capsule] Image positionnée : {position_presentateur}")
113
  v_presentateur = img_clip
114
  except Exception as e:
115
  print(f"[Capsule] ❌ Erreur image présentateur : {e}")
116
-
117
- # --- Ajout final ---
118
  if v_presentateur:
119
- print(f"[Capsule] ✅ Présentateur ajouté (image ou vidéo)")
120
  clips.append(v_presentateur)
121
- else:
122
- print(f"[Capsule] ❌ Échec préparation présentateur")
123
  else:
124
- print(f"[Capsule] Aucune vidéo/image présentateur: {video_presentateur}")
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
 
127
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
128
  name = safe_name(f"{titre}_{langue}")
129
  out_base = os.path.join(OUT_DIR, name)
@@ -143,11 +163,12 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
143
  })
144
  _save_manifest()
145
 
 
146
  try:
147
  audio.close()
148
  final.close()
149
  bg.close()
150
- if 'v_presentateur' in locals() and v_presentateur is not None:
151
  v_presentateur.close()
152
  if os.path.exists(audio_mp):
153
  os.remove(audio_mp)
@@ -159,12 +180,15 @@ def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
159
 
160
  return out, f"✅ Capsule {langue.upper()} créée ({dur:.1f}s, voix {speaker or voix_type})", srt_path
161
 
 
 
 
162
  def table_capsules():
163
- import os
164
  return [[i+1, c["title"], c.get("langue","fr").upper(),
165
  f"{c['duration']}s", c["theme"], c["voice"], os.path.basename(c["file"])]
166
  for i, c in enumerate(capsules)]
167
 
 
168
  def assemble_final():
169
  if not capsules:
170
  return None, "❌ Aucune capsule."
@@ -183,6 +207,7 @@ def assemble_final():
183
  try: c.close()
184
  except: pass
185
 
 
186
  def supprimer_capsule(index):
187
  try:
188
  idx = int(index) - 1
@@ -198,6 +223,7 @@ def supprimer_capsule(index):
198
  except Exception as e:
199
  return f"❌ Erreur lors de la suppression : {e}", table_capsules()
200
 
 
201
  def deplacer_capsule(index, direction):
202
  try:
203
  idx = int(index) - 1
@@ -211,56 +237,40 @@ def deplacer_capsule(index, direction):
211
  return f"❌ Erreur de déplacement : {e}", table_capsules()
212
 
213
 
214
- import shutil, time
215
-
216
  def export_project_zip():
217
  """Crée un ZIP complet pour toutes les capsules (vidéos, srt, inputs, params, manifest)."""
218
  try:
219
  zip_root = os.path.join(TMP_DIR, f"zip_export_{int(time.time())}")
220
  os.makedirs(zip_root, exist_ok=True)
221
- # Manifest global
222
  manifest = {"capsules": capsules}
223
  with open(os.path.join(zip_root, "manifest.json"), "w", encoding="utf-8") as f:
224
  json.dump(manifest, f, ensure_ascii=False, indent=2)
225
 
226
- # Par capsule
227
  for i, cap in enumerate(capsules, 1):
228
  cap_dir = os.path.join(zip_root, f"capsule_{i}")
229
  os.makedirs(os.path.join(cap_dir, "inputs"), exist_ok=True)
230
-
231
- # copy video + srt if exists
232
- for key in ["file"]:
233
- path = cap.get(key)
234
- if path and os.path.exists(path):
235
- shutil.copy2(path, os.path.join(cap_dir, os.path.basename(path)))
236
- # try to find srt next to file if not stored
237
- if cap.get("file"):
238
- base = os.path.splitext(os.path.basename(cap["file"]))[0]
239
- # srt names are not strictly tied; we copy any srt in OUT_DIR too
240
  for f in os.listdir(OUT_DIR):
241
  if f.lower().endswith(".srt"):
242
  shutil.copy2(os.path.join(OUT_DIR, f), os.path.join(cap_dir, f))
243
-
244
- # inputs
245
  for k in ["image_fond", "logo", "video_presentateur"]:
246
  p = cap.get("inputs", {}).get(k) if cap.get("inputs") else None
247
  if p and os.path.exists(p):
248
  shutil.copy2(p, os.path.join(cap_dir, "inputs", os.path.basename(p)))
249
-
250
- # params.json per capsule
251
  with open(os.path.join(cap_dir, "params.json"), "w", encoding="utf-8") as pf:
252
  json.dump(cap, pf, ensure_ascii=False, indent=2)
253
 
254
- # zip
255
  zip_path = shutil.make_archive(os.path.join(TMP_DIR, f"capsules_export_{int(time.time())}"), "zip", zip_root)
256
  return zip_path
257
  except Exception as e:
258
  print(f"[Export] ❌ Erreur ZIP : {e}")
259
  return None
260
 
 
261
  def assemble_final_fast():
262
  """Concaténation instantanée sans réencodage (FFmpeg direct)."""
263
- import subprocess, os
264
  if not capsules:
265
  return None, "❌ Aucune capsule."
266
  list_path = os.path.join(OUT_DIR, "concat_list.txt")
@@ -268,10 +278,6 @@ def assemble_final_fast():
268
  for c in capsules:
269
  f.write(f"file '{c['file']}'\n")
270
  out_path = os.path.join(OUT_DIR, "VIDEO_COMPLETE_FAST.mp4")
271
- cmd = [
272
- "ffmpeg", "-f", "concat", "-safe", "0", "-i", list_path,
273
- "-c", "copy", out_path
274
- ]
275
  subprocess.run(cmd, check=True)
276
  return out_path, "🎉 Vidéo finale assemblée instantanément (sans réencodage)"
277
-
 
1
+ import os, json, uuid, gc, traceback, shutil, time
2
+ from moviepy.editor import ImageClip, AudioFileClip, CompositeVideoClip, TextClip
3
  from moviepy.video.VideoClip import ColorClip
4
 
5
  from .config import OUT_DIR, TMP_DIR, MANIFEST_PATH, W, H
 
9
 
10
  capsules = []
11
 
12
+ # Charger le manifeste existant
13
  if os.path.exists(MANIFEST_PATH):
14
  try:
15
  data = json.load(open(MANIFEST_PATH, "r", encoding="utf-8"))
 
18
  except Exception:
19
  pass
20
 
21
+
22
  def _save_manifest():
23
  with open(MANIFEST_PATH, "w", encoding="utf-8") as f:
24
  json.dump({"capsules": capsules}, f, ensure_ascii=False, indent=2)
25
 
26
+
27
  def build_capsule(titre, sous_titre, texte_voix, texte_ecran, theme,
28
  image_fond=None, logo_path=None, logo_pos="haut-gauche",
29
+ fond_mode="plein écran", video_presentateur=None,
30
+ voix_type="Féminine", position_presentateur="bottom-right",
31
+ plein=False, moteur_voix="Edge-TTS (recommandé)",
32
+ langue="fr", speaker=None):
33
 
34
+ # 1️⃣ TTS
35
  try:
36
  audio_mp = tts_edge(texte_voix, voice=speaker or ("fr-FR-DeniseNeural" if langue == "fr" else "nl-NL-MaaikeNeural"))
37
  except Exception as e:
 
45
  except Exception as e:
46
  print(f"[Audio] Normalisation échouée ({e}), on garde {audio_mp}")
47
 
48
+ # 2️⃣ Fond
49
  print("[Capsule] Génération du fond...")
50
  fond_path = make_background(titre, sous_titre, texte_ecran, theme,
51
  logo_path, logo_pos, image_fond, fond_mode)
52
+
53
+ if fond_path and not os.path.exists(fond_path):
54
+ # Correction chemin TMP_DIR
55
+ alt_path = os.path.join(os.getcwd(), "_tmp_capsules", os.path.basename(fond_path))
56
+ if os.path.exists(alt_path):
57
+ fond_path = alt_path
58
+ print(f"[Capsule] ⚙️ Correction du chemin fond: {fond_path}")
59
+
60
  if fond_path is None:
61
  print("[Capsule] ❌ fond_path est None, création d'urgence")
62
  from PIL import Image
 
69
  print(f"[Capsule] ❌ Impossible de créer le fond d'urgence: {e}")
70
  fond_path = None
71
 
72
+ # 3️⃣ Composition vidéo
73
  audio = AudioFileClip(audio_wav)
74
  dur = float(audio.duration or 5.0)
75
  target_fps = 25
 
89
  bg = ColorClip(size=(W, H), color=(0, 82, 147)).set_duration(dur)
90
  clips.append(bg)
91
 
92
+ # 4️⃣ Présentateur
93
+ v_presentateur = None
94
+ has_text = bool(texte_ecran and texte_ecran.strip())
95
+ plein_auto = plein if not has_text else False
96
+
97
  if video_presentateur and os.path.exists(video_presentateur):
98
  ext = os.path.splitext(video_presentateur)[1].lower()
 
 
 
99
  if ext in [".mp4", ".mov", ".avi", ".mkv"]:
100
  print(f"[Capsule] Vidéo présentateur trouvée: {video_presentateur}")
101
  v_presentateur = prepare_video_presentateur(
102
+ video_presentateur, dur, position_presentateur, plein_auto
103
  )
 
 
104
  elif ext in [".jpg", ".jpeg", ".png", ".bmp", ".webp"]:
 
105
  print(f"[Capsule] Image présentateur trouvée: {video_presentateur}")
106
  try:
107
  img_clip = ImageClip(video_presentateur).set_duration(dur)
108
+ if plein_auto:
109
  img_clip = img_clip.resize((W, H)).set_position(("center", "center"))
 
110
  else:
111
  img_clip = img_clip.resize(width=520)
112
  pos_map = {
 
117
  "center": ("center", "center"),
118
  }
119
  img_clip = img_clip.set_position(pos_map.get(position_presentateur, ("right", "bottom")))
 
120
  v_presentateur = img_clip
121
  except Exception as e:
122
  print(f"[Capsule] ❌ Erreur image présentateur : {e}")
123
+
 
124
  if v_presentateur:
 
125
  clips.append(v_presentateur)
126
+ print(f"[Capsule] ✅ Présentateur ajouté")
 
127
  else:
128
+ print(f"[Capsule] Aucun présentateur: {video_presentateur}")
129
 
130
+ # 5️⃣ Overlay texte (par-dessus tout)
131
+ if texte_ecran and texte_ecran.strip():
132
+ try:
133
+ txt_clip = TextClip(
134
+ texte_ecran,
135
+ fontsize=50,
136
+ color='white',
137
+ font='DejaVu-Sans-Bold',
138
+ method='caption',
139
+ size=(W - 200, None)
140
+ ).set_position(('center', 'bottom')).set_duration(dur)
141
+ clips.append(txt_clip)
142
+ print("[Capsule] ✅ Overlay texte ajouté (bas centré)")
143
+ except Exception as e:
144
+ print(f"[Capsule] ⚠️ Overlay texte échoué : {e}")
145
 
146
+ # 6️⃣ Assemblage final
147
  final = CompositeVideoClip(clips).set_audio(audio.set_fps(44100))
148
  name = safe_name(f"{titre}_{langue}")
149
  out_base = os.path.join(OUT_DIR, name)
 
163
  })
164
  _save_manifest()
165
 
166
+ # 7️⃣ Nettoyage
167
  try:
168
  audio.close()
169
  final.close()
170
  bg.close()
171
+ if v_presentateur:
172
  v_presentateur.close()
173
  if os.path.exists(audio_mp):
174
  os.remove(audio_mp)
 
180
 
181
  return out, f"✅ Capsule {langue.upper()} créée ({dur:.1f}s, voix {speaker or voix_type})", srt_path
182
 
183
+
184
+ # 🧾 Fonctions supplémentaires identiques à ta version originale
185
+
186
  def table_capsules():
 
187
  return [[i+1, c["title"], c.get("langue","fr").upper(),
188
  f"{c['duration']}s", c["theme"], c["voice"], os.path.basename(c["file"])]
189
  for i, c in enumerate(capsules)]
190
 
191
+
192
  def assemble_final():
193
  if not capsules:
194
  return None, "❌ Aucune capsule."
 
207
  try: c.close()
208
  except: pass
209
 
210
+
211
  def supprimer_capsule(index):
212
  try:
213
  idx = int(index) - 1
 
223
  except Exception as e:
224
  return f"❌ Erreur lors de la suppression : {e}", table_capsules()
225
 
226
+
227
  def deplacer_capsule(index, direction):
228
  try:
229
  idx = int(index) - 1
 
237
  return f"❌ Erreur de déplacement : {e}", table_capsules()
238
 
239
 
 
 
240
  def export_project_zip():
241
  """Crée un ZIP complet pour toutes les capsules (vidéos, srt, inputs, params, manifest)."""
242
  try:
243
  zip_root = os.path.join(TMP_DIR, f"zip_export_{int(time.time())}")
244
  os.makedirs(zip_root, exist_ok=True)
 
245
  manifest = {"capsules": capsules}
246
  with open(os.path.join(zip_root, "manifest.json"), "w", encoding="utf-8") as f:
247
  json.dump(manifest, f, ensure_ascii=False, indent=2)
248
 
 
249
  for i, cap in enumerate(capsules, 1):
250
  cap_dir = os.path.join(zip_root, f"capsule_{i}")
251
  os.makedirs(os.path.join(cap_dir, "inputs"), exist_ok=True)
252
+ if cap.get("file") and os.path.exists(cap["file"]):
253
+ shutil.copy2(cap["file"], os.path.join(cap_dir, os.path.basename(cap["file"])))
 
 
 
 
 
 
 
 
254
  for f in os.listdir(OUT_DIR):
255
  if f.lower().endswith(".srt"):
256
  shutil.copy2(os.path.join(OUT_DIR, f), os.path.join(cap_dir, f))
 
 
257
  for k in ["image_fond", "logo", "video_presentateur"]:
258
  p = cap.get("inputs", {}).get(k) if cap.get("inputs") else None
259
  if p and os.path.exists(p):
260
  shutil.copy2(p, os.path.join(cap_dir, "inputs", os.path.basename(p)))
 
 
261
  with open(os.path.join(cap_dir, "params.json"), "w", encoding="utf-8") as pf:
262
  json.dump(cap, pf, ensure_ascii=False, indent=2)
263
 
 
264
  zip_path = shutil.make_archive(os.path.join(TMP_DIR, f"capsules_export_{int(time.time())}"), "zip", zip_root)
265
  return zip_path
266
  except Exception as e:
267
  print(f"[Export] ❌ Erreur ZIP : {e}")
268
  return None
269
 
270
+
271
  def assemble_final_fast():
272
  """Concaténation instantanée sans réencodage (FFmpeg direct)."""
273
+ import subprocess
274
  if not capsules:
275
  return None, "❌ Aucune capsule."
276
  list_path = os.path.join(OUT_DIR, "concat_list.txt")
 
278
  for c in capsules:
279
  f.write(f"file '{c['file']}'\n")
280
  out_path = os.path.join(OUT_DIR, "VIDEO_COMPLETE_FAST.mp4")
281
+ cmd = ["ffmpeg", "-f", "concat", "-safe", "0", "-i", list_path, "-c", "copy", out_path]
 
 
 
282
  subprocess.run(cmd, check=True)
283
  return out_path, "🎉 Vidéo finale assemblée instantanément (sans réencodage)"