feat(reels): montage Remotion unifié, aperçu en direct, tests et CI

Montage (étape 3) :
- Nouvelle composition ReelVideo : sous-titres animés mot à mot (3 styles :
  Impact, Surligné, Épuré), logo, effet de fin et fondu, en React
- Le service Python prépare l'image (recadrage, HDR, stabilisation, dernière
  image figée) et la piste son finale (/prepare-reel) ; Remotion compose
- Reels d'images sur les mêmes composants, avec le vrai minutage de la voix
- Police Montserrat embarquée (plus de dépendance à Google Fonts)
- REEL_RENDERER=ffmpeg conserve le rendu FFmpeg, plus rapide, en secours
- Script de pré-bundle réparé (échouait en silence : require en ESM)

Interface (étape 4) :
- Aperçu en direct avec @remotion/player, identique au rendu final ; la voix
  testée cale les sous-titres, sinon minutage estimé
- Choix du style de sous-titres sur les 4 pages Reel
- Vraie progression : étape réelle du rendu, échecs visibles 24 h avec leur
  cause ; fin de la barre simulée et du faux « publié avec succès »

Outillage (étape 5) :
- Tests vitest (minutage identique à Python, validation, sécurité) et
  pytest ; CI GitHub Actions (tsc, tests, build, ruff)
- Captures d'écran, out.mp4 et scripts de test retirés de la racine

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 14:37:42 +00:00
1 parent 3c6a66bf64
commit 9b66a6dc5a
64 files changed
+2488 -782

No files matched your search

+9 -2
View File
@@ -8,7 +8,8 @@ import { storage } from '../storage';
import { ffmpegService, FFmpegServiceError } from '../services/ffmpeg';
import { ttsPreviewSchema, videoReelParamsSchema, type VideoReelParams } from '@shared/reel';
import { enqueueReelJob, countActiveReelJobs } from '../services/reels/queue';
import { resolveGeminiApiKey, resolveStoreName } from '../services/reels/assets';
import { resolveGeminiApiKey, resolveLogoPath, resolveStoreName } from '../services/reels/assets';
import { configuredRenderer } from '../services/reels/videoPipeline';
import { openRouterService, describeGenerationError } from '../services/openrouter';
import { estimateVoiceTiming } from '../services/ttsSync';
@@ -378,12 +379,18 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
*/
reelsRouter.get('/reels/config', async (req: Request, res: Response) => {
try {
const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false);
const [ffmpegConfigured, logoUrl] = await Promise.all([
ffmpegService.healthCheck().catch(() => false),
resolveLogoPath(),
]);
res.json({
jamendo: { configured: false },
freesound: { configured: false },
ffmpeg: { configured: ffmpegConfigured },
// Aperçu : logo affiché par le lecteur, moteur de rendu utilisé
logoUrl: logoUrl ?? null,
renderer: configuredRenderer(),
});
} catch (error) {
console.error('❌ Error fetching Reels config:', error);
+1
View File
@@ -74,6 +74,7 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
ttsEngine: req.body.ttsEngine || undefined,
ttsVoice: req.body.ttsVoice,
ttsStyle: req.body.ttsStyle || undefined,
captionStyle: req.body.captionStyle || undefined,
storeName: await resolveStoreName(user.id, req.body.selectedPageId),
tempFiles: uploaded.map((f) => f.path),
});