fix: vignettes vidéo persistantes et remise à zéro des erreurs de types

Deux corrections demandées ensemble.

1. Vignettes vidéo

Le planificateur supprime la vidéo locale une fois publiée, alors que la
ligne `media` survit pour l'historique. L'interface tentait donc de lire
un fichier disparu pour en extraire une image : d'où les avertissements
« Video thumbnail generation failed » et les tuiles vides.

Une vignette JPEG est désormais extraite à la création du reel, dans les
deux flux de génération, et stockée à part : elle reste disponible quand
la vidéo ne l'est plus. La purge quotidienne la nettoie en même temps que
la ligne qu'elle illustre. L'extraction est tolérante à l'échec — une
vignette manquante dégrade l'affichage mais ne fait jamais échouer une
publication — et le composant retombe sur son icône de remplacement pour
les médias créés avant ce changement. La fonction d'extraction, jusque-là
privée à la route Remotion, devient un service partagé.

2. Erreurs de compilation préexistantes (27 → 0)

- remotion-video.tsx (bureau et mobile) : les composants Select étaient
  utilisés sans être importés, ce qui faisait planter le sélecteur de voix
  à l'exécution.
- external.ts : fileSize et aiGenerated étaient transmis dans un type que
  le schéma ne déclare pas, et ne fonctionnaient que par conversion
  implicite de PostgreSQL.
- reels.ts : l'interface MusicTrack, référencée mais jamais déclarée dans
  ce module.
- remotion.ts : chromiumOptions.args retiré. Remotion ne lit jamais cette
  clé, les drapeaux Chrome qu'elle contenait n'atteignaient donc pas le
  navigateur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
This commit is contained in:
Claude committed 2026-08-12 08:00:48 +00:00
1 parent c69fe4e462
commit ac8b494f24
16 files changed
+173 -25

No files matched your search

+8 -21
View File
@@ -2,8 +2,6 @@ import { Router } from "express";
import multer from "multer";
import path from "path";
import fs from "fs";
import { exec } from "child_process";
import { promisify } from "util";
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import { ffmpegService } from "../services/ffmpeg";
@@ -11,26 +9,9 @@ import { storage as dbStorage } from "../storage";
import { minioService as cloudinaryService, buildMinioUrl } from "../services/minio";
import { facebookService } from "../services/facebook";
import { tiktokService } from "../services/tiktok";
import { generateVideoThumbnail, createVideoThumbnail } from "../services/thumbnail";
import * as musicMetadata from "music-metadata";
const execAsync = promisify(exec);
/**
* Generate a thumbnail from a video file using FFmpeg
* @param videoPath Path to the video file
* @param outputPath Path to save the thumbnail
* @param seekTime Time in seconds to extract the frame (default: 1s)
*/
async function generateVideoThumbnail(videoPath: string, outputPath: string, seekTime: number = 1): Promise<boolean> {
try {
const cmd = `ffmpeg -y -ss ${seekTime} -i "${videoPath}" -vframes 1 -q:v 2 "${outputPath}"`;
await execAsync(cmd);
return fs.existsSync(outputPath);
} catch (error) {
console.warn('⚠️ Failed to generate video thumbnail:', error);
return false;
}
}
export const remotionRouter = Router();
@@ -326,10 +307,12 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
codec: "h264",
outputLocation,
inputProps,
// Ne pas y remettre "args" : Remotion ne lit jamais cette clé (elle est
// absente de ChromiumOptions et de son code), les drapeaux passés là
// n'atteignaient donc jamais le navigateur.
chromiumOptions: {
disableWebSecurity: true,
ignoreCertificateErrors: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
},
concurrency: 1, // Limit concurrency to prevent Docker memory exhaustion
});
@@ -408,6 +391,9 @@ remotionRouter.post("/publish", async (req, res) => {
});
// Create a Media record for the processed video
// Vignette extraite avant que la vidéo ne soit purgée du disque
const thumbnailUrl = await createVideoThumbnail(localPath);
const mediaRecord = await dbStorage.createMedia({
userId: user.id,
type: "video",
@@ -416,6 +402,7 @@ remotionRouter.post("/publish", async (req, res) => {
facebookFeedUrl: null,
instagramFeedUrl: null,
instagramStoryUrl: null,
thumbnailUrl,
fileName: filename,
fileSize: fs.statSync(localPath).size,
});