mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 23:18:12 +02:00
Merge pull request #124 from R0m1k3/claude/practical-newton-aal4xl
Reels : option « Publier aussi en story » sur Facebook
This commit is contained in:
10 files changed
+172
-5
No files matched your search
@@ -0,0 +1,29 @@
|
|||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
|
interface StoryToggleProps {
|
||||||
|
checked: boolean;
|
||||||
|
onCheckedChange: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Double le Reel d'une story sur les pages Facebook sélectionnées. TikTok n'a
|
||||||
|
* pas de stories : l'option ne le concerne pas.
|
||||||
|
*/
|
||||||
|
export function StoryToggle({ checked, onCheckedChange }: StoryToggleProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-lg border border-border/50 p-3">
|
||||||
|
<label htmlFor="reel-also-story" className="space-y-0.5 cursor-pointer">
|
||||||
|
<div className="text-sm font-medium">Publier aussi en story</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
La vidéo est aussi publiée en story sur les pages Facebook sélectionnées (visible 24 h).
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id="reel-also-story"
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(value) => onCheckedChange(value === true)}
|
||||||
|
data-testid="switch-reel-also-story"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { StoryToggle } from "@/components/reels/story-toggle";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -83,6 +84,7 @@ export default function MobileNewReel() {
|
|||||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||||
// Désactivée par défaut : double le temps de rendu, utile seulement pour une vidéo tremblée
|
// Désactivée par défaut : double le temps de rendu, utile seulement pour une vidéo tremblée
|
||||||
const [stabilize, setStabilize] = useState(false);
|
const [stabilize, setStabilize] = useState(false);
|
||||||
|
const [alsoStory, setAlsoStory] = useState(false);
|
||||||
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
||||||
const [showLogo, setShowLogo] = useState(true);
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
|
|
||||||
@@ -272,6 +274,7 @@ export default function MobileNewReel() {
|
|||||||
description: srtFile ? srtText(srtFile.cues) : overlayText,
|
description: srtFile ? srtText(srtFile.cues) : overlayText,
|
||||||
pageIds: selectedPages,
|
pageIds: selectedPages,
|
||||||
scheduledFor: scheduledDate?.toISOString(),
|
scheduledFor: scheduledDate?.toISOString(),
|
||||||
|
alsoStory,
|
||||||
musicVolume: musicVolume[0] / 100,
|
musicVolume: musicVolume[0] / 100,
|
||||||
stabilize: stabilize,
|
stabilize: stabilize,
|
||||||
drawText: true,
|
drawText: true,
|
||||||
@@ -610,6 +613,7 @@ export default function MobileNewReel() {
|
|||||||
Pages Facebook
|
Pages Facebook
|
||||||
</div>
|
</div>
|
||||||
{facebookPages.map(renderTargetCheckbox)}
|
{facebookPages.map(renderTargetCheckbox)}
|
||||||
|
<StoryToggle checked={alsoStory} onCheckedChange={setAlsoStory} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { StoryToggle } from "@/components/reels/story-toggle";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -78,6 +79,7 @@ export default function NewReel() {
|
|||||||
const [drawText, setDrawText] = useState(true);
|
const [drawText, setDrawText] = useState(true);
|
||||||
// Désactivée par défaut : double le temps de rendu, utile seulement pour une vidéo tremblée
|
// Désactivée par défaut : double le temps de rendu, utile seulement pour une vidéo tremblée
|
||||||
const [stabilize, setStabilize] = useState(false);
|
const [stabilize, setStabilize] = useState(false);
|
||||||
|
const [alsoStory, setAlsoStory] = useState(false);
|
||||||
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
||||||
const [showLogo, setShowLogo] = useState(true);
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
|
|
||||||
@@ -415,6 +417,7 @@ export default function NewReel() {
|
|||||||
description: srtFile ? srtText(srtFile.cues) : overlayText,
|
description: srtFile ? srtText(srtFile.cues) : overlayText,
|
||||||
pageIds: selectedPages,
|
pageIds: selectedPages,
|
||||||
scheduledFor: scheduledDate?.toISOString(),
|
scheduledFor: scheduledDate?.toISOString(),
|
||||||
|
alsoStory,
|
||||||
musicVolume: musicVolume[0] / 100,
|
musicVolume: musicVolume[0] / 100,
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
ttsEngine,
|
ttsEngine,
|
||||||
@@ -938,6 +941,7 @@ export default function NewReel() {
|
|||||||
Pages Facebook
|
Pages Facebook
|
||||||
</div>
|
</div>
|
||||||
{facebookPages.map(renderTargetCheckbox)}
|
{facebookPages.map(renderTargetCheckbox)}
|
||||||
|
<StoryToggle checked={alsoStory} onCheckedChange={setAlsoStory} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { StoryToggle } from "@/components/reels/story-toggle";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
|
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
|
||||||
import type { SocialPage, Media } from "@shared/schema";
|
import type { SocialPage, Media } from "@shared/schema";
|
||||||
@@ -30,6 +31,7 @@ export default function ScheduleReel() {
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [selectedPages, setSelectedPages] = useState<string[]>([]);
|
const [selectedPages, setSelectedPages] = useState<string[]>([]);
|
||||||
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [alsoStory, setAlsoStory] = useState(false);
|
||||||
|
|
||||||
const { data: pages = [] } = useQuery<SocialPage[]>({ queryKey: ["/api/pages"] });
|
const { data: pages = [] } = useQuery<SocialPage[]>({ queryKey: ["/api/pages"] });
|
||||||
const { data: allMedia = [] } = useQuery<Media[]>({ queryKey: ["/api/media"] });
|
const { data: allMedia = [] } = useQuery<Media[]>({ queryKey: ["/api/media"] });
|
||||||
@@ -83,6 +85,7 @@ export default function ScheduleReel() {
|
|||||||
mediaIds: [selectedVideo!.id],
|
mediaIds: [selectedVideo!.id],
|
||||||
pageIds: selectedPages,
|
pageIds: selectedPages,
|
||||||
scheduledFor: scheduledDate!.toISOString(),
|
scheduledFor: scheduledDate!.toISOString(),
|
||||||
|
alsoStory,
|
||||||
});
|
});
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
@@ -250,6 +253,7 @@ export default function ScheduleReel() {
|
|||||||
<SiFacebook className="w-4 h-4 text-[#1877F2]" /> Pages Facebook
|
<SiFacebook className="w-4 h-4 text-[#1877F2]" /> Pages Facebook
|
||||||
</Label>
|
</Label>
|
||||||
{facebookPages.map(renderTargetCheckbox)}
|
{facebookPages.map(renderTargetCheckbox)}
|
||||||
|
<StoryToggle checked={alsoStory} onCheckedChange={setAlsoStory} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{tiktokAccounts.length > 0 && (
|
{tiktokAccounts.length > 0 && (
|
||||||
|
|||||||
+13
-1
@@ -1268,7 +1268,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const { pageIds, postType, mediaId, mediaIds, ...postFields } = req.body;
|
const { pageIds, postType, mediaId, mediaIds, alsoStory, ...postFields } = req.body;
|
||||||
|
|
||||||
// Convert mediaIds to standardized format: array of { mediaId, displayOrder }
|
// Convert mediaIds to standardized format: array of { mediaId, displayOrder }
|
||||||
let finalMediaItems: Array<{ mediaId: string; displayOrder: number }> = [];
|
let finalMediaItems: Array<{ mediaId: string; displayOrder: number }> = [];
|
||||||
@@ -1395,6 +1395,18 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
postType: finalPostType,
|
postType: finalPostType,
|
||||||
scheduledAt,
|
scheduledAt,
|
||||||
});
|
});
|
||||||
|
// Reel doublé d'une story : seulement sur Facebook, TikTok n'en a pas
|
||||||
|
if (finalPostType === 'reel' && alsoStory === true) {
|
||||||
|
const page = await storage.getSocialPage(pageId);
|
||||||
|
if (page?.platform === 'facebook') {
|
||||||
|
await storage.createScheduledPost({
|
||||||
|
postId: post.id,
|
||||||
|
pageId,
|
||||||
|
postType: 'story',
|
||||||
|
scheduledAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -514,6 +514,61 @@ export class FacebookService {
|
|||||||
return finishData.post_id || videoId;
|
return finishData.post_id || videoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie une vidéo en story Facebook en envoyant directement les données
|
||||||
|
* binaires (start → upload → finish sur /video_stories). Sert à doubler un
|
||||||
|
* Reel d'une story : la vidéo rendue est déjà en mémoire.
|
||||||
|
*/
|
||||||
|
async publishVideoStoryFromBuffer(page: SocialPage, videoBuffer: Buffer): Promise<string> {
|
||||||
|
if (!page.accessToken) throw new Error('No access token found for this page');
|
||||||
|
const accessToken = page.accessToken;
|
||||||
|
const fileSize = videoBuffer.length;
|
||||||
|
|
||||||
|
const startParams = new URLSearchParams({
|
||||||
|
access_token: accessToken,
|
||||||
|
upload_phase: 'start',
|
||||||
|
});
|
||||||
|
const startRes = await fetch(`${this.baseUrl}/${page.pageId}/video_stories?${startParams.toString()}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (!startRes.ok) {
|
||||||
|
const error = await startRes.json() as FacebookError;
|
||||||
|
throw new Error(`Facebook Story API error (START phase): ${error.error.message} (code: ${error.error.code})`);
|
||||||
|
}
|
||||||
|
const { video_id: videoId, upload_url: uploadUrl } = await startRes.json() as { video_id: string; upload_url: string };
|
||||||
|
|
||||||
|
const uploadRes = await fetch(uploadUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `OAuth ${accessToken}`,
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'offset': '0',
|
||||||
|
'file_size': fileSize.toString(),
|
||||||
|
},
|
||||||
|
body: new Blob([videoBuffer], { type: 'video/mp4' }),
|
||||||
|
});
|
||||||
|
if (!uploadRes.ok) {
|
||||||
|
throw new Error(`Facebook Story API error (UPLOAD phase): ${await uploadRes.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishParams = new URLSearchParams({
|
||||||
|
access_token: accessToken,
|
||||||
|
upload_phase: 'finish',
|
||||||
|
video_id: videoId,
|
||||||
|
});
|
||||||
|
const finishRes = await fetch(`${this.baseUrl}/${page.pageId}/video_stories?${finishParams.toString()}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (!finishRes.ok) {
|
||||||
|
const error = await finishRes.json() as FacebookError;
|
||||||
|
throw new Error(`Facebook Story API error (FINISH phase): ${error.error.message} (code: ${error.error.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishData = await finishRes.json() as { success: boolean; post_id?: string };
|
||||||
|
console.log(`✅ Story vidéo publiée sur ${page.pageName}`);
|
||||||
|
return finishData.post_id || videoId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publie une vidéo comme Reel Facebook
|
* Publie une vidéo comme Reel Facebook
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* cas de l'ancien `publishReel(originalUrl)`).
|
* cas de l'ancien `publishReel(originalUrl)`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Media } from "@shared/schema";
|
import type { Media, SocialPage } from "@shared/schema";
|
||||||
import { storage } from "../../storage";
|
import { storage } from "../../storage";
|
||||||
import { minioService } from "../minio";
|
import { minioService } from "../minio";
|
||||||
import { createVideoThumbnail } from "../thumbnail";
|
import { createVideoThumbnail } from "../thumbnail";
|
||||||
@@ -19,6 +19,8 @@ export interface PagePublishResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
reelId?: string;
|
reelId?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
/** Échec de la story associée : n'empêche pas le Reel d'être publié. */
|
||||||
|
storyError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Range la vidéo rendue dans la médiathèque, avec sa vignette. */
|
/** Range la vidéo rendue dans la médiathèque, avec sa vignette. */
|
||||||
@@ -49,6 +51,9 @@ export async function storeRenderedVideo(
|
|||||||
/**
|
/**
|
||||||
* Publie (ou planifie) la vidéo sur chaque page, crée les entrées
|
* Publie (ou planifie) la vidéo sur chaque page, crée les entrées
|
||||||
* `scheduled_posts` et met à jour le statut du post.
|
* `scheduled_posts` et met à jour le statut du post.
|
||||||
|
*
|
||||||
|
* Avec `alsoStory`, chaque page Facebook reçoit aussi la vidéo en story (TikTok
|
||||||
|
* n'a pas de stories).
|
||||||
*/
|
*/
|
||||||
export async function publishReelToPages(options: {
|
export async function publishReelToPages(options: {
|
||||||
postId: string;
|
postId: string;
|
||||||
@@ -56,8 +61,9 @@ export async function publishReelToPages(options: {
|
|||||||
videoBuffer: Buffer;
|
videoBuffer: Buffer;
|
||||||
description: string;
|
description: string;
|
||||||
scheduledFor?: string;
|
scheduledFor?: string;
|
||||||
|
alsoStory?: boolean;
|
||||||
}): Promise<PagePublishResult[]> {
|
}): Promise<PagePublishResult[]> {
|
||||||
const { postId, pageIds, videoBuffer, description, scheduledFor } = options;
|
const { postId, pageIds, videoBuffer, description, scheduledFor, alsoStory } = options;
|
||||||
const results: PagePublishResult[] = [];
|
const results: PagePublishResult[] = [];
|
||||||
|
|
||||||
for (const pageId of pageIds) {
|
for (const pageId of pageIds) {
|
||||||
@@ -82,9 +88,18 @@ export async function publishReelToPages(options: {
|
|||||||
postType: "reel",
|
postType: "reel",
|
||||||
scheduledAt: scheduledFor ? new Date(scheduledFor) : new Date(),
|
scheduledAt: scheduledFor ? new Date(scheduledFor) : new Date(),
|
||||||
});
|
});
|
||||||
|
const withStory = alsoStory && page.platform === "facebook";
|
||||||
|
|
||||||
if (scheduledFor) {
|
if (scheduledFor) {
|
||||||
// Le planificateur publiera à l'heure dite
|
// Le planificateur publiera à l'heure dite
|
||||||
|
if (withStory) {
|
||||||
|
await storage.createScheduledPost({
|
||||||
|
postId,
|
||||||
|
pageId: page.id,
|
||||||
|
postType: "story",
|
||||||
|
scheduledAt: new Date(scheduledFor),
|
||||||
|
});
|
||||||
|
}
|
||||||
results.push({ pageId, success: true, reelId: "scheduled" });
|
results.push({ pageId, success: true, reelId: "scheduled" });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -96,7 +111,8 @@ export async function publishReelToPages(options: {
|
|||||||
publishedAt: new Date(),
|
publishedAt: new Date(),
|
||||||
externalPostId: reelId,
|
externalPostId: reelId,
|
||||||
});
|
});
|
||||||
results.push({ pageId, success: true, reelId });
|
const storyError = withStory ? await publishStory(postId, page, videoBuffer) : undefined;
|
||||||
|
results.push({ pageId, success: true, reelId, storyError });
|
||||||
} else {
|
} else {
|
||||||
// TikTok finalise la publication de son côté : l'envoi est marqué comme
|
// TikTok finalise la publication de son côté : l'envoi est marqué comme
|
||||||
// effectué pour ne pas republier, et le poller de statut renseignera
|
// effectué pour ne pas republier, et le poller de statut renseignera
|
||||||
@@ -126,6 +142,46 @@ export async function publishReelToPages(options: {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie la story d'une page juste après son Reel : un échec est consigné sur
|
||||||
|
* son entrée planifiée (visible dans l'historique) et renvoyé, sans faire
|
||||||
|
* échouer le Reel.
|
||||||
|
*/
|
||||||
|
async function publishStory(
|
||||||
|
postId: string,
|
||||||
|
page: SocialPage,
|
||||||
|
videoBuffer: Buffer,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
// Créée seulement maintenant : en attente pendant l'envoi du Reel, le
|
||||||
|
// planificateur aurait pu la publier une seconde fois.
|
||||||
|
let scheduledStoryId: string | undefined;
|
||||||
|
try {
|
||||||
|
({ id: scheduledStoryId } = await storage.createScheduledPost({
|
||||||
|
postId,
|
||||||
|
pageId: page.id,
|
||||||
|
postType: "story",
|
||||||
|
scheduledAt: new Date(),
|
||||||
|
}));
|
||||||
|
const storyId = await facebookService.publishVideoStoryFromBuffer(page, videoBuffer);
|
||||||
|
await storage.updateScheduledPost(scheduledStoryId, {
|
||||||
|
publishedAt: new Date(),
|
||||||
|
externalPostId: storyId,
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Erreur inconnue";
|
||||||
|
console.error(`❌ [Reels] Story sur ${page.pageName} impossible :`, error);
|
||||||
|
if (scheduledStoryId) {
|
||||||
|
// Marquée comme traitée pour que le planificateur ne la republie pas en boucle
|
||||||
|
await storage.updateScheduledPost(scheduledStoryId, {
|
||||||
|
publishedAt: new Date(),
|
||||||
|
error: message,
|
||||||
|
}).catch((e) => console.error(`⚠️ [Reels] Échec de la story non enregistré :`, e));
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Message d'erreur lisible lorsque toutes les publications ont échoué. */
|
/** Message d'erreur lisible lorsque toutes les publications ont échoué. */
|
||||||
export function describePublishFailure(results: PagePublishResult[]): string {
|
export function describePublishFailure(results: PagePublishResult[]): string {
|
||||||
const details = results.map((r) => r.error).filter(Boolean).join(" ; ");
|
const details = results.map((r) => r.error).filter(Boolean).join(" ; ");
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export async function runVideoReelJob({ job, progress }: JobContext) {
|
|||||||
videoBuffer,
|
videoBuffer,
|
||||||
description: params.description || params.overlayText || "",
|
description: params.description || params.overlayText || "",
|
||||||
scheduledFor: params.scheduledFor,
|
scheduledFor: params.scheduledFor,
|
||||||
|
alsoStory: params.alsoStory,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!results.some((r) => r.success)) {
|
if (!results.some((r) => r.success)) {
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ describe("videoReelParamsSchema", () => {
|
|||||||
|
|
||||||
it("applique les valeurs par défaut", () => {
|
it("applique les valeurs par défaut", () => {
|
||||||
const params = videoReelParamsSchema.parse({ videoMediaId: "v", pageIds: ["p"] });
|
const params = videoReelParamsSchema.parse({ videoMediaId: "v", pageIds: ["p"] });
|
||||||
expect(params).toMatchObject({ captionStyle: "bold", stabilize: false, drawText: true, musicVolume: 0.25, showLogo: true });
|
expect(params).toMatchObject({ captionStyle: "bold", stabilize: false, drawText: true, musicVolume: 0.25, showLogo: true, alsoStory: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepte les anciens paramètres encore stockés dans des jobs", () => {
|
it("accepte les anciens paramètres encore stockés dans des jobs", () => {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const videoReelParamsSchema = z.object({
|
|||||||
ttsEngine: z.enum(TTS_ENGINES).optional(),
|
ttsEngine: z.enum(TTS_ENGINES).optional(),
|
||||||
ttsStyle: z.enum(TTS_STYLES).optional(),
|
ttsStyle: z.enum(TTS_STYLES).optional(),
|
||||||
scheduledFor: optionalText,
|
scheduledFor: optionalText,
|
||||||
|
// Publie aussi la vidéo en story sur les pages Facebook (sans effet sur TikTok)
|
||||||
|
alsoStory: z.boolean().default(false),
|
||||||
// Ancien réglage, ignoré : le minutage vient désormais de la voix elle-même
|
// Ancien réglage, ignoré : le minutage vient désormais de la voix elle-même
|
||||||
wordDuration: z.number().optional(),
|
wordDuration: z.number().optional(),
|
||||||
fontSize: z.number().int().min(16).max(200).default(64),
|
fontSize: z.number().int().min(16).max(200).default(64),
|
||||||
|
|||||||
Reference in new issue
Block a user