mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-16 03:39:15 +02:00
feat(reels): file d'attente persistante des rendus (reel_jobs)
L'ancienne file comptait les posts « processing » : un redémarrage pendant un rendu la bloquait définitivement. Les rendus passent désormais par une table reel_jobs, réservée avec FOR UPDATE SKIP LOCKED, avec heartbeat et reprise des jobs interrompus au démarrage (2 tentatives maximum). - Pipeline vidéo et pipeline images (Remotion) extraits des routes vers server/services/reels/, les routes ne font que valider (zod) et mettre en file - Rendus d'images suivis en base au lieu d'une Map en mémoire - storeName conservé pour les jobs mis en attente - Publication Facebook en binaire : l'URL relative /uploads/... transmise auparavant n'était pas téléchargeable par Facebook - Job en échec si toutes les pages échouent ou si la voix demandée manque (le service Python renseigne enfin tts_error) - La voix choisie sur la page images est réellement utilisée - sync-info mesure la voix Gemini avec sa clé - Timeouts sur les appels au service FFmpeg, vignettes via execFile, /debug-ffmpeg protégé par la clé API, plus de voix anglaise en secours Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
This commit is contained in:
19 files changed
+1071
-901
No files matched your search
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Paramètres des rendus de Reels, validés à l'entrée des routes et stockés tels
|
||||
* quels dans `reel_jobs.params` : un job repris après redémarrage retrouve ainsi
|
||||
* exactement ce que l'utilisateur avait demandé.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
const optionalText = z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((value) => (value ? value : undefined));
|
||||
|
||||
/** Reel construit à partir d'une vidéo de la médiathèque. */
|
||||
export const videoReelParamsSchema = z.object({
|
||||
videoMediaId: z.string({ required_error: "Vidéo requise" }).min(1, "Vidéo requise"),
|
||||
pageIds: z
|
||||
.array(z.string().min(1), { required_error: "Au moins une page requise" })
|
||||
.min(1, "Au moins une page requise"),
|
||||
musicTrackId: optionalText,
|
||||
musicUrl: optionalText,
|
||||
overlayText: optionalText,
|
||||
description: optionalText,
|
||||
ttsEnabled: z.boolean().default(false),
|
||||
ttsVoice: optionalText,
|
||||
ttsEngine: z.enum(["gemini", "edge"]).optional(),
|
||||
scheduledFor: optionalText,
|
||||
wordDuration: z.number().positive().max(5).default(0.6),
|
||||
fontSize: z.number().int().min(16).max(200).default(64),
|
||||
musicVolume: z.number().min(0).max(2).default(0.25),
|
||||
drawText: z.boolean().default(true),
|
||||
stabilize: z.boolean().default(false),
|
||||
enableEndingEffect: z.boolean().default(true),
|
||||
// Déterminé par le serveur à partir de la première page, jamais par le client
|
||||
storeName: z.string().optional(),
|
||||
});
|
||||
|
||||
export type VideoReelParams = z.infer<typeof videoReelParamsSchema>;
|
||||
|
||||
/** Reel construit à partir d'images (rendu Remotion). */
|
||||
export const imagesReelParamsSchema = z.object({
|
||||
// Chemins relatifs /uploads/... ou URL absolues
|
||||
imageUrls: z.array(z.string().min(1)).min(1, "Aucune image fournie").max(4),
|
||||
overlayText: optionalText,
|
||||
musicUrl: optionalText,
|
||||
musicVolume: z.number().min(0).max(2).default(0.3),
|
||||
ttsEngine: z.enum(["gemini", "edge"]).optional(),
|
||||
ttsVoice: optionalText,
|
||||
storeName: z.string().optional(),
|
||||
// Fichiers temporaires à supprimer une fois le rendu terminé
|
||||
tempFiles: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export type ImagesReelParams = z.infer<typeof imagesReelParamsSchema>;
|
||||
|
||||
export type ReelJobKind = "video" | "images";
|
||||
|
||||
export type ReelJobStatus = "pending" | "processing" | "completed" | "failed";
|
||||
|
||||
/** Résultat d'un rendu d'images, consulté par le client pour l'aperçu. */
|
||||
export interface ImagesReelResult {
|
||||
url: string;
|
||||
thumbnailUrl: string | null;
|
||||
}
|
||||
+27
-1
@@ -281,6 +281,31 @@ export const userPagePermissions = pgTable("user_page_permissions", {
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
|
||||
/**
|
||||
* File d'attente persistante des rendus de Reels.
|
||||
*
|
||||
* Un job survit à un redémarrage du serveur : le worker le réserve avec
|
||||
* `FOR UPDATE SKIP LOCKED`, entretient `heartbeat_at` pendant le rendu, et les
|
||||
* jobs dont le cœur ne bat plus sont remis en file au démarrage suivant.
|
||||
*/
|
||||
export const reelJobs = pgTable("reel_jobs", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
// Post suivi par l'interface (reel vidéo). Absent pour un rendu d'images non publié.
|
||||
postId: varchar("post_id").references(() => posts.id, { onDelete: "cascade" }),
|
||||
kind: text("kind").notNull(), // 'video' | 'images'
|
||||
params: jsonb("params").notNull(),
|
||||
status: text("status").notNull().default("pending"), // 'pending' | 'processing' | 'completed' | 'failed'
|
||||
step: text("step"),
|
||||
progress: integer("progress").notNull().default(0),
|
||||
attempts: integer("attempts").notNull().default(0),
|
||||
result: jsonb("result"),
|
||||
error: text("error"),
|
||||
heartbeatAt: timestamp("heartbeat_at"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const socialPagesRelations = relations(socialPages, ({ one, many }) => ({
|
||||
user: one(users, {
|
||||
fields: [socialPages.userId],
|
||||
@@ -558,4 +583,5 @@ export type TiktokConfig = typeof tiktokConfig.$inferSelect;
|
||||
export type InsertTiktokConfig = z.infer<typeof insertTiktokConfigSchema>;
|
||||
export type UpdateTiktokConfig = z.infer<typeof updateTiktokConfigSchema>;
|
||||
|
||||
|
||||
export type ReelJob = typeof reelJobs.$inferSelect;
|
||||
export type InsertReelJob = typeof reelJobs.$inferInsert;
|
||||
Reference in new issue
Block a user