Files
Socialflow/server/services/reels/remotionAssets.ts
T
Claude 9b66a6dc5a feat(reels): montage Remotion unifié, aperçu en direct, tests et CI
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
2026-09-24 14:37:42 +00:00

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;
}
}