mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-20 05:28:13 +02:00
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:
16 files changed
+173
-25
No files matched your search
@@ -137,7 +137,7 @@ router.post("/publish", async (req, res) => {
|
||||
instagramFeedUrl: uploaded.instagramFeedUrl,
|
||||
instagramStoryUrl: uploaded.instagramStoryUrl,
|
||||
fileName,
|
||||
fileSize: String(buffer.length),
|
||||
fileSize: buffer.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ router.post("/publish", async (req, res) => {
|
||||
content,
|
||||
status: scheduledFor ? "scheduled" : "scheduled",
|
||||
scheduledFor: scheduledFor ?? new Date(),
|
||||
aiGenerated: false,
|
||||
aiGenerated: "false",
|
||||
});
|
||||
|
||||
// Lier le média si présent
|
||||
@@ -415,7 +415,7 @@ router.patch("/posts/:id", async (req, res) => {
|
||||
instagramFeedUrl: uploaded.instagramFeedUrl,
|
||||
instagramStoryUrl: uploaded.instagramStoryUrl,
|
||||
fileName,
|
||||
fileSize: String(buffer.length),
|
||||
fileSize: buffer.length,
|
||||
});
|
||||
|
||||
// Remplacer les médias existants par le nouveau
|
||||
|
||||
@@ -8,6 +8,7 @@ import { storage } from '../storage';
|
||||
import { ffmpegService } from '../services/ffmpeg';
|
||||
import { facebookService } from '../services/facebook';
|
||||
import { tiktokService } from '../services/tiktok';
|
||||
import { createVideoThumbnail } from '../services/thumbnail';
|
||||
import { minioService as cloudinaryService, buildMinioUrl, resolveInternalUrl } from '../services/minio';
|
||||
import { openRouterService, describeGenerationError } from '../services/openrouter';
|
||||
import { db } from '../db';
|
||||
@@ -15,6 +16,19 @@ import { cloudinaryConfig } from '@shared/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { ttsSyncService } from '../services/ttsSync';
|
||||
/** Piste musicale telle qu'attendue par le client. */
|
||||
interface MusicTrack {
|
||||
id: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
albumName: string;
|
||||
duration: number;
|
||||
previewUrl: string;
|
||||
downloadUrl: string;
|
||||
imageUrl: string;
|
||||
license: string;
|
||||
}
|
||||
|
||||
export const reelsRouter = Router();
|
||||
|
||||
// ============================================
|
||||
@@ -616,6 +630,10 @@ async function processReelBackground(
|
||||
console.log('✅ [Background] Uploaded:', cloudinaryResult.originalUrl);
|
||||
|
||||
// 5. Créer l'enregistrement Media pour la vidéo traitée
|
||||
// La vignette est extraite maintenant : la vidéo sera supprimée du disque
|
||||
// après publication, mais l'historique doit rester illustré.
|
||||
const thumbnailUrl = await createVideoThumbnail(videoBuffer);
|
||||
|
||||
const processedMedia = await storage.createMedia({
|
||||
userId: userId,
|
||||
type: 'video',
|
||||
@@ -624,6 +642,7 @@ async function processReelBackground(
|
||||
facebookFeedUrl: cloudinaryResult.facebookFeedUrl || null,
|
||||
instagramFeedUrl: cloudinaryResult.instagramFeedUrl || null,
|
||||
instagramStoryUrl: cloudinaryResult.instagramStoryUrl || null,
|
||||
thumbnailUrl,
|
||||
fileName: `reel-processed-${Date.now()}.mp4`,
|
||||
fileSize: videoBuffer.length,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user