feat(reels): file d'attente persistante des rendus (reel_jobs)

L'ancienne file comptait les posts « processing » : un redémarrage pendant
un rendu la bloquait définitivement. Les rendus passent désormais par une
table reel_jobs, réservée avec FOR UPDATE SKIP LOCKED, avec heartbeat et
reprise des jobs interrompus au démarrage (2 tentatives maximum).

- Pipeline vidéo et pipeline images (Remotion) extraits des routes vers
  server/services/reels/, les routes ne font que valider (zod) et mettre en file
- Rendus d'images suivis en base au lieu d'une Map en mémoire
- storeName conservé pour les jobs mis en attente
- Publication Facebook en binaire : l'URL relative /uploads/... transmise
  auparavant n'était pas téléchargeable par Facebook
- Job en échec si toutes les pages échouent ou si la voix demandée manque
  (le service Python renseigne enfin tts_error)
- La voix choisie sur la page images est réellement utilisée
- sync-info mesure la voix Gemini avec sa clé
- Timeouts sur les appels au service FFmpeg, vignettes via execFile,
  /debug-ffmpeg protégé par la clé API, plus de voix anglaise en secours

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
This commit is contained in:
Claude committed 2026-09-24 09:31:31 +00:00
1 parent fcbc6b706f
commit d24ab0d4b7
19 files changed
+1071 -901

No files matched your search

+7 -3
View File
@@ -46,7 +46,9 @@ run_diagnostics()
@app.get("/debug-ffmpeg")
async def debug_ffmpeg():
async def debug_ffmpeg(x_api_key: str = Header(None)):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API Key")
try:
filters = subprocess.run(
["ffmpeg", "-filters"], capture_output=True, text=True
@@ -57,7 +59,6 @@ async def debug_ffmpeg():
"subtitles": "subtitles" in filters,
"drawtext": "drawtext" in filters,
},
"env": {k: v for k, v in os.environ.items() if "API" not in k},
"fonts": fonts.splitlines()[:50], # First 50
"raw_filters_hint": filters[:500],
}
@@ -337,7 +338,6 @@ async def generate_tts_with_subs(
"fr-FR-DeniseNeural",
]
fallback_voices.append("en-US-JennyNeural")
fallback_voices = list(dict.fromkeys(fallback_voices))
last_error = None
@@ -902,6 +902,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
# 3. Generate TTS (if enabled)
has_tts = False
tts_error = None
tts_clean_text = ""
if request.tts_enabled and request.text:
@@ -966,12 +967,14 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
has_tts = True
else:
print("❌ TTS audio file missing or empty!")
tts_error = "Fichier audio de la voix vide"
else:
print("⚠️ TTS text is empty after cleaning, skipping.")
except Exception as e:
import traceback
print(f"❌ Failed to generate TTS: {e}")
traceback.print_exc()
tts_error = str(e) or type(e).__name__
stats["tts_duration"] = time.time() - start_step
start_step = time.time()
@@ -1298,6 +1301,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
"output_base64": out_b64,
"duration": duration,
"processing_stats": stats,
"tts_error": tts_error,
}
except Exception as e: