mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-17 20:26:24 +02:00
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
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
/**
|
|
* Ressources fournies aux compositions Remotion pendant un rendu serveur.
|
|
*/
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
/** Dossier temporaire servi sous /uploads/temp (voir server/index.ts). */
|
|
export const REMOTION_TEMP_DIR =
|
|
process.env.TEMP_UPLOAD_DIR || path.join(process.cwd(), "uploads", "temp");
|
|
|
|
/**
|
|
* URL HTTP locale d'un fichier servi par l'application. Vidéo et audio sont
|
|
* lus côté Node par le moteur de rendu : l'adresse locale suffit.
|
|
*/
|
|
export function localHttpUrl(url: string): string {
|
|
if (!url.startsWith("/")) return url;
|
|
const port = process.env.PORT || "5555";
|
|
return `http://localhost:${port}${url}`;
|
|
}
|
|
|
|
/** URL servie d'un fichier du dossier temporaire. */
|
|
export function tempFileUrl(absolutePath: string): string {
|
|
const relative = path.relative(REMOTION_TEMP_DIR, absolutePath).split(path.sep).join("/");
|
|
return localHttpUrl(`/uploads/temp/${relative}`);
|
|
}
|
|
|
|
const MIME_BY_EXT: Record<string, string> = {
|
|
png: "image/png",
|
|
gif: "image/gif",
|
|
webp: "image/webp",
|
|
svg: "image/svg+xml",
|
|
};
|
|
|
|
/**
|
|
* Convertit un chemin local /uploads/... en data URL : les images sont
|
|
* affichées par Chromium, qui ne lit pas http://localhost de façon fiable
|
|
* dans Docker. Refuse tout chemin sortant du dossier uploads.
|
|
*/
|
|
export async function toDataUrl(relativeUrl: string): Promise<string> {
|
|
if (!relativeUrl.startsWith("/uploads/")) return relativeUrl;
|
|
const uploadsRoot = path.join(process.cwd(), "uploads") + path.sep;
|
|
const tempRoot = path.resolve(REMOTION_TEMP_DIR) + path.sep;
|
|
const filePath = relativeUrl.startsWith("/uploads/temp/")
|
|
? path.resolve(REMOTION_TEMP_DIR, "." + relativeUrl.slice("/uploads/temp".length))
|
|
: path.resolve(process.cwd(), "." + relativeUrl);
|
|
if (!filePath.startsWith(uploadsRoot) && !filePath.startsWith(tempRoot)) {
|
|
throw new Error(`Chemin d'image refusé : ${relativeUrl}`);
|
|
}
|
|
try {
|
|
const buffer = await fs.promises.readFile(filePath);
|
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
return `data:${MIME_BY_EXT[ext] ?? "image/jpeg"};base64,${buffer.toString("base64")}`;
|
|
} catch {
|
|
console.warn(`⚠️ [Remotion] Image introuvable : ${filePath}`);
|
|
return relativeUrl;
|
|
}
|
|
}
|