Spaces:
Sleeping
Sleeping
| import os, re, uuid, traceback | |
| from moviepy.editor import VideoFileClip | |
| import moviepy.video.fx.all as vfx | |
| from .config import OUT_DIR, W, H | |
| def safe_name(stem, ext=".mp4"): | |
| stem = re.sub(r"[^\w\-]+", "_", stem)[:40] | |
| return f"{stem}_{uuid.uuid4().hex[:6]}{ext}" | |
| def prepare_video_presentateur(video_path, audio_duration, position, plein_ecran=False): | |
| """Prépare la vidéo du présentateur avec la bonne durée et position.""" | |
| try: | |
| print(f"[Video] Chargement: {video_path}") | |
| if not os.path.exists(video_path): | |
| print(f"[Video] ❌ Fichier introuvable: {video_path}") | |
| return None | |
| v = VideoFileClip(video_path).without_audio() | |
| print(f"[Video] Durée vidéo: {v.duration}s, Audio: {audio_duration}s") | |
| if v.duration < audio_duration: | |
| print(f"[Video] Bouclage nécessaire ({v.duration}s -> {audio_duration}s)") | |
| v = v.fx(vfx.loop, duration=audio_duration) | |
| elif v.duration > audio_duration: | |
| print(f"[Video] Découpage nécessaire ({v.duration}s -> {audio_duration}s)") | |
| v = v.subclip(0, audio_duration) | |
| if plein_ecran: | |
| print(f"[Video] Mode plein écran") | |
| v = v.resize(newsize=(W, H)).set_position(("center", "center")) | |
| else: | |
| print(f"[Video] Mode incrustation, position: {position}") | |
| v = v.resize(width=520) | |
| pos_map = { | |
| "bottom-right": ("right", "bottom"), | |
| "bottom-left": ("left", "bottom"), | |
| "top-right": ("right", "top"), | |
| "top-left": ("left", "top"), | |
| "center": ("center", "center"), | |
| } | |
| v = v.set_position(pos_map.get(position, ("right", "bottom"))) | |
| print(f"[Video] ✅ Vidéo préparée avec succès") | |
| return v | |
| except Exception as e: | |
| print(f"[Video] ❌ Erreur préparation: {e}") | |
| print(f"[Video] Traceback: {traceback.format_exc()}") | |
| return None | |
| def write_srt(text, duration, base_name=None): | |
| """ | |
| Crée un fichier SRT synchronisé avec la durée audio. | |
| Si base_name est fourni, le fichier SRT porte ce nom fixe. | |
| """ | |
| parts = re.split(r'(?<=[\.!?])\s+', text.strip()) | |
| parts = [p for p in parts if p] | |
| total = len("".join(parts)) or 1 | |
| cur = 0.0 | |
| srt = [] | |
| for i, p in enumerate(parts, 1): | |
| prop = len(p) / total | |
| start = cur | |
| end = min(duration, cur + duration * prop) | |
| cur = end | |
| def ts(t): | |
| m, s = divmod(t, 60) | |
| h, m = divmod(m, 60) | |
| return f"{int(h):02}:{int(m):02}:{int(s):02},000" | |
| srt += [f"{i}", f"{ts(start)} --> {ts(end)}", p, ""] | |
| # Nom du fichier SRT | |
| if base_name: | |
| safe_base = re.sub(r'[^\w\-]+', '_', base_name)[:40] | |
| path = os.path.join(OUT_DIR, f"{safe_base}.srt") | |
| else: | |
| path = os.path.join(OUT_DIR, f"srt_default.srt") | |
| # Écriture du fichier | |
| with open(path, "w", encoding="utf-8") as f: | |
| f.write("\n".join(srt)) | |
| print(f"[SRT] ✅ Fichier SRT généré : {path}") | |
| return path | |
| def write_video_with_fallback(final_clip, out_path_base, fps=25): | |
| attempts = [ | |
| {"ext": ".mp4", "codec": "libx264", "audio_codec": "aac"}, | |
| {"ext": ".mp4", "codec": "mpeg4", "audio_codec": "aac"}, | |
| {"ext": ".mp4", "codec": "libx264", "audio_codec": "libmp3lame"}, | |
| ] | |
| ffmpeg_params = ["-pix_fmt", "yuv420p", "-movflags", "+faststart", "-threads", "1", "-shortest"] | |
| last_err = None | |
| for opt in attempts: | |
| out = out_path_base if out_path_base.endswith(opt["ext"]) else out_path_base + opt["ext"] | |
| try: | |
| final_clip.write_videofile( | |
| out, | |
| fps=fps, | |
| codec=opt["codec"], | |
| audio_codec=opt["audio_codec"], | |
| audio=True, | |
| ffmpeg_params=ffmpeg_params, | |
| logger=None, | |
| threads=os.cpu_count(), # ✅ multi-threading activé | |
| ) | |
| if os.path.exists(out) and os.path.getsize(out) > 150000: | |
| return out | |
| except Exception as e: | |
| last_err = f"{type(e).__name__}: {e}" | |
| raise RuntimeError(last_err or "FFmpeg a échoué") | |