Aller au contenu principal

Video Generator

Vue d'ensemble

Le microservice dedicace-generator génère des vidéos de présentation à partir des messages texte et vidéo collectés.

Stack Technique

  • Runtime: Node.js 20
  • Framework: Express.js
  • Vidéo: FFmpeg + fluent-ffmpeg
  • Rendu HTML: Puppeteer
  • Images: Sharp
  • Container: Docker avec Chromium

Structure du Projet

dedicace-generator/
├── main.js # Point d'entrée principal
├── package.json
├── Dockerfile
├── deploy.sh # Script de déploiement
├── presentations/ # Vidéos générées (volume)
├── backgrounds/ # Images de fond
└── templates/ # Templates HTML

API Endpoints

POST /create_presentation

Crée une nouvelle présentation vidéo.

Request:

{
"title": "Anniversaire de Marie",
"description": "Tous vos messages pour Marie",
"pages": [
{
"id": 1,
"type": "text",
"content": "<p>Joyeux anniversaire !</p>",
"author": "Jean"
},
{
"id": 2,
"type": "video",
"content": "https://xxx.supabase.co/storage/v1/object/sign/video_dedicace/..."
}
],
"webhook_url": "https://app.momentscollectifs.fr/api/webhook"
}

Response:

{
"presentationId": "1701234567890_abc123",
"status": "processing"
}

GET /download/:presentationId/:filename

Télécharge une vidéo générée.

Paramètres:

  • presentationId: ID de la présentation
  • filename: desktop.mp4 ou mobile.mp4

Response: Fichier MP4 en streaming

GET /health

Vérifie l'état du service.

Response:

{
"status": "healthy",
"ffmpeg": true,
"puppeteer": true
}

Pipeline de Génération

Étapes Détaillées

1. Validation et Préparation

app.post('/create_presentation', async (req, res) => {
const { title, description, pages, webhook_url } = req.body;

// Validation
if (!title || !pages || pages.length === 0) {
return res.status(400).json({ error: 'Invalid input' });
}

// Création ID unique
const presentationId = `${Date.now()}_${randomId()}`;
const workDir = path.join(PRESENTATIONS_DIR, presentationId);

await fs.mkdir(workDir, { recursive: true });

// Réponse immédiate
res.json({ presentationId, status: 'processing' });

// Traitement asynchrone
processPresentation(presentationId, { title, description, pages, webhook_url });
});

2. Téléchargement des Vidéos

async function downloadVideo(url, destPath) {
// Essai avec axios
try {
const response = await axios.get(url, { responseType: 'stream' });
const writer = fs.createWriteStream(destPath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
return;
} catch (e) {
console.log('axios failed, trying curl...');
}

// Fallback curl
await execPromise(`curl -L -o "${destPath}" "${url}"`);
}

3. Rendu Texte vers Vidéo

async function renderTextToVideo(content, author, outputPath, format) {
const { width, height } = format === 'desktop'
? { width: 1920, height: 1080 }
: { width: 1080, height: 1920 };

// HTML template
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body {
width: ${width}px;
height: ${height}px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Noto Sans', sans-serif;
}
.content {
max-width: 80%;
color: white;
text-align: center;
font-size: 48px;
}
.author {
margin-top: 40px;
font-size: 32px;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="content">
${content}
<div class="author">— ${author}</div>
</div>
</body>
</html>
`;

// Puppeteer screenshot
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium',
args: ['--no-sandbox']
});

const page = await browser.newPage();
await page.setViewport({ width, height });
await page.setContent(html);

const imagePath = outputPath.replace('.mp4', '.png');
await page.screenshot({ path: imagePath });
await browser.close();

// Image vers vidéo (5 secondes)
await new Promise((resolve, reject) => {
ffmpeg()
.input(imagePath)
.loop(5)
.outputOptions([
'-c:v libx264',
'-t 5',
'-pix_fmt yuv420p',
'-r 30'
])
.output(outputPath)
.on('end', resolve)
.on('error', reject)
.run();
});
}

4. Merge des Segments

async function mergeVideos(inputPaths, outputPath) {
const listFile = outputPath.replace('.mp4', '_list.txt');

// Créer fichier de liste pour FFmpeg
const listContent = inputPaths
.map(p => `file '${p}'`)
.join('\n');
await fs.writeFile(listFile, listContent);

// Merge avec FFmpeg
await new Promise((resolve, reject) => {
ffmpeg()
.input(listFile)
.inputOptions(['-f concat', '-safe 0'])
.outputOptions([
'-c:v libx264',
'-c:a aac',
'-strict experimental'
])
.output(outputPath)
.on('end', resolve)
.on('error', reject)
.run();
});

await fs.unlink(listFile);
}

Formats de Sortie

Desktop (1920x1080)

  • Résolution : 1920 x 1080 (16:9)
  • Codec vidéo : H.264
  • Codec audio : AAC
  • Framerate : 30 fps

Mobile (1080x1920)

  • Résolution : 1080 x 1920 (9:16)
  • Codec vidéo : H.264
  • Codec audio : AAC
  • Framerate : 30 fps

Configuration Docker

Dockerfile

FROM node:20-bookworm

# Dépendances système
RUN apt-get update && apt-get install -y \
ffmpeg \
chromium \
fonts-noto-color-emoji \
fonts-noto-cjk \
libvips-dev \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

# Configuration Puppeteer
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Volumes pour persistance
VOLUME ["/app/presentations", "/app/backgrounds"]

EXPOSE 5001

CMD ["npm", "start"]

docker-compose.yml

version: '3.8'

services:
dedicace-generator:
build: .
ports:
- "5001:5001"
volumes:
- ./presentations:/app/presentations
- ./backgrounds:/app/backgrounds
environment:
- NODE_ENV=production
- PORT=5001
- SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY}
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.generator.rule=Host(`api.momentscollectifs.fr`)"
- "traefik.http.routers.generator.tls=true"

Variables d'Environnement

VariableDescriptionDéfaut
PORTPort du serveur5001
NODE_ENVEnvironnementdevelopment
SUPABASE_SERVICE_KEYClé service Supabase-
PUPPETEER_EXECUTABLE_PATHChemin Chromium/usr/bin/chromium

Commandes

# Développement
npm run dev # Avec nodemon

# Production
npm start # Serveur Express

# Docker
docker build -t dedicace-generator .
docker run -d -p 5001:5001 \
-v $(pwd)/presentations:/app/presentations \
-v $(pwd)/backgrounds:/app/backgrounds \
--name dedicace-generator \
dedicace-generator

# Déploiement
./deploy.sh

Gestion des Erreurs

Webhook de Notification

async function notifyWebhook(webhookUrl, presentationId, status, error = null) {
try {
await axios.post(webhookUrl, {
presentationId,
status, // 'completed' | 'error'
error,
downloadUrls: status === 'completed' ? {
desktop: `/download/${presentationId}/desktop.mp4`,
mobile: `/download/${presentationId}/mobile.mp4`
} : null
});
} catch (e) {
console.error('Webhook notification failed:', e.message);
}
}

Codes d'Erreur

CodeDescription
400Paramètres invalides
404Présentation non trouvée
500Erreur de génération
503Service indisponible (FFmpeg/Puppeteer)