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ésentationfilename:desktop.mp4oumobile.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
| Variable | Description | Défaut |
|---|---|---|
PORT | Port du serveur | 5001 |
NODE_ENV | Environnement | development |
SUPABASE_SERVICE_KEY | Clé service Supabase | - |
PUPPETEER_EXECUTABLE_PATH | Chemin 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
| Code | Description |
|---|---|
| 400 | Paramètres invalides |
| 404 | Présentation non trouvée |
| 500 | Erreur de génération |
| 503 | Service indisponible (FFmpeg/Puppeteer) |