diff --git a/client/src/components/reels/reel-preview.tsx b/client/src/components/reels/reel-preview.tsx
index 70d461d..ac66911 100644
--- a/client/src/components/reels/reel-preview.tsx
+++ b/client/src/components/reels/reel-preview.tsx
@@ -32,6 +32,8 @@ interface CommonProps {
musicUrl?: string;
musicVolume: number;
logoUrl?: string | null;
+ /** Petit logo pendant la vidéo (le grand logo de fin dépend de l'effet de fin). */
+ showWatermark: boolean;
storeName?: string;
endingEffect: boolean;
/** Sous-titres SRT : remplacent `text`, minutés par le fichier. */
@@ -74,7 +76,8 @@ function estimatedVoiceDuration(text: string): number {
*/
export function ReelPreview(props: ReelPreviewProps) {
const videoDuration = useVideoDuration(props.kind === "video" ? props.videoUrl : undefined);
- const { text, showCaptions, captionStyle, ttsEnabled, voice, musicUrl, musicVolume, endingEffect } = props;
+ const { text, showCaptions, captionStyle, ttsEnabled, voice, musicUrl, musicVolume, endingEffect, showWatermark } =
+ props;
const logoUrl = props.logoUrl ?? undefined;
const storeName = endingEffect ? props.storeName || undefined : undefined;
const srtCues = props.kind === "video" && props.srtCues?.length ? props.srtCues : null;
@@ -106,6 +109,7 @@ export function ReelPreview(props: ReelPreviewProps) {
musicUrl,
musicVolume,
logoUrl,
+ showWatermark,
storeName,
endingSeconds,
};
@@ -136,6 +140,7 @@ export function ReelPreview(props: ReelPreviewProps) {
words,
captionStyle,
logoUrl,
+ showWatermark,
storeName,
logoStart: timing.logoStart,
fadeStart: timing.fadeStart,
@@ -150,7 +155,7 @@ export function ReelPreview(props: ReelPreviewProps) {
props.kind,
props.kind === "video" ? props.videoUrl : props.images.join("|"),
videoDuration, text, cleanText, srtCues, showCaptions, captionStyle, ttsEnabled, voice, voiceDuration,
- musicUrl, musicVolume, logoUrl, storeName, endingEffect,
+ musicUrl, musicVolume, logoUrl, showWatermark, storeName, endingEffect,
]);
if (!composition) {
diff --git a/client/src/pages/mobile/new-reel.tsx b/client/src/pages/mobile/new-reel.tsx
index 83d51cc..47f7cbd 100644
--- a/client/src/pages/mobile/new-reel.tsx
+++ b/client/src/pages/mobile/new-reel.tsx
@@ -84,6 +84,7 @@ export default function MobileNewReel() {
// 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 [enableEndingEffect, setEnableEndingEffect] = useState(true);
+ const [showLogo, setShowLogo] = useState(true);
// TTS Sync state
const [syncInfo, setSyncInfo] = useState<{
@@ -280,6 +281,7 @@ export default function MobileNewReel() {
ttsStyle,
captionStyle,
enableEndingEffect,
+ showLogo,
});
};
@@ -484,6 +486,16 @@ export default function MobileNewReel() {
+
+
+
+
p.id === selectedPages[0])?.pageName}
endingEffect={enableEndingEffect}
/>
diff --git a/client/src/pages/mobile/remotion-video.tsx b/client/src/pages/mobile/remotion-video.tsx
index b0e5e2c..d0a4a51 100644
--- a/client/src/pages/mobile/remotion-video.tsx
+++ b/client/src/pages/mobile/remotion-video.tsx
@@ -54,6 +54,7 @@ export default function MobileRemotionVideoPage() {
const [publishDescription, setPublishDescription] = useState("");
const [isPublishing, setIsPublishing] = useState(false);
const [captionStyle, setCaptionStyle] = useState(DEFAULT_CAPTION_STYLE);
+ const [showLogo, setShowLogo] = useState(true);
const [voicePreview, setVoicePreview] = useState(null);
const [renderProgress, setRenderProgress] = useState(0);
const currentVoice = isVoicePreviewCurrent(voicePreview, overlayText, voiceSettings) ? voicePreview : null;
@@ -169,6 +170,7 @@ export default function MobileRemotionVideoPage() {
formData.append("ttsVoice", voiceSettings.voice);
formData.append("ttsStyle", voiceSettings.style);
formData.append("captionStyle", captionStyle);
+ formData.append("showLogo", String(showLogo));
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); }
@@ -350,6 +352,10 @@ export default function MobileRemotionVideoPage() {
+
+
+
+
@@ -474,6 +480,7 @@ export default function MobileRemotionVideoPage() {
musicUrl={musicFileUrl ?? selectedTrack?.url}
musicVolume={musicVolume}
logoUrl={reelConfig?.logoUrl}
+ showWatermark={showLogo}
storeName={socialPages.find((p) => p.id === selectedPageIds[0])?.pageName ?? socialPages[0]?.pageName}
endingEffect
/>
diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx
index ec5eb92..1ea3554 100644
--- a/client/src/pages/new-reel.tsx
+++ b/client/src/pages/new-reel.tsx
@@ -79,6 +79,7 @@ export default function NewReel() {
// 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 [enableEndingEffect, setEnableEndingEffect] = useState(true);
+ const [showLogo, setShowLogo] = useState(true);
// TTS Sync state
const [syncInfo, setSyncInfo] = useState<{
@@ -423,6 +424,7 @@ export default function NewReel() {
drawText,
stabilize: stabilize,
enableEndingEffect,
+ showLogo,
});
};
@@ -818,6 +820,17 @@ export default function NewReel() {
)}
+
+
+
+
+
p.id === selectedPages[0])?.pageName}
endingEffect={enableEndingEffect}
/>
diff --git a/client/src/pages/remotion-video.tsx b/client/src/pages/remotion-video.tsx
index dc555ba..a80b9cf 100644
--- a/client/src/pages/remotion-video.tsx
+++ b/client/src/pages/remotion-video.tsx
@@ -55,6 +55,7 @@ export default function RemotionVideoPage() {
const [publishDescription, setPublishDescription] = useState("");
const [isPublishing, setIsPublishing] = useState(false);
const [captionStyle, setCaptionStyle] = useState(DEFAULT_CAPTION_STYLE);
+ const [showLogo, setShowLogo] = useState(true);
const [voicePreview, setVoicePreview] = useState(null);
const [renderProgress, setRenderProgress] = useState(0);
const currentVoice = isVoicePreviewCurrent(voicePreview, overlayText, voiceSettings) ? voicePreview : null;
@@ -135,6 +136,7 @@ export default function RemotionVideoPage() {
formData.append("ttsVoice", voiceSettings.voice);
formData.append("ttsStyle", voiceSettings.style);
formData.append("captionStyle", captionStyle);
+ formData.append("showLogo", String(showLogo));
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); }
@@ -290,6 +292,10 @@ export default function RemotionVideoPage() {
+
+
+
+
@@ -420,6 +426,7 @@ export default function RemotionVideoPage() {
musicUrl={musicFileUrl ?? selectedTrack?.url}
musicVolume={musicVolume}
logoUrl={reelConfig?.logoUrl}
+ showWatermark={showLogo}
storeName={socialPages.find((p) => p.id === selectedPageIds[0])?.pageName ?? socialPages[0]?.pageName}
endingEffect
/>
diff --git a/client/src/remotion/ImageComposition.tsx b/client/src/remotion/ImageComposition.tsx
index 015a1b0..4c4616f 100644
--- a/client/src/remotion/ImageComposition.tsx
+++ b/client/src/remotion/ImageComposition.tsx
@@ -14,6 +14,8 @@ export type ImageCompositionProps = {
musicUrl?: string;
musicVolume?: number;
logoUrl?: string;
+ /** Petit logo affiché pendant la vidéo (le grand logo de fin reste sur la diapositive de fin). */
+ showWatermark?: boolean;
storeName?: string;
/** Durée de la diapositive de fin (logo + nom du magasin), en secondes. */
endingSeconds?: number;
@@ -78,7 +80,7 @@ const ImageSlide: React.FC<{ src: string; effectIndex: number }> = ({ src, effec
/** Reel à partir d'images : diaporama animé, voix, sous-titres et diapositive de fin. */
export const ImageComposition: React.FC = ({
- images, words, captionStyle, audioUrl, musicUrl, musicVolume = 0.3, logoUrl, storeName, endingSeconds = 3,
+ images, words, captionStyle, audioUrl, musicUrl, musicVolume = 0.3, logoUrl, showWatermark = true, storeName, endingSeconds = 3,
}) => {
const { fps, durationInFrames } = useVideoConfig();
const hasEnding = Boolean(logoUrl || storeName) && endingSeconds > 0;
@@ -113,7 +115,7 @@ export const ImageComposition: React.FC = ({
)}
- {logoUrl && }
+ {logoUrl && showWatermark && }
{hasEnding && }
);
diff --git a/client/src/remotion/ReelVideo.tsx b/client/src/remotion/ReelVideo.tsx
index 32c7d56..df884d3 100644
--- a/client/src/remotion/ReelVideo.tsx
+++ b/client/src/remotion/ReelVideo.tsx
@@ -12,6 +12,8 @@ export type ReelVideoProps = {
words: TimedWord[];
captionStyle: CaptionStyle;
logoUrl?: string;
+ /** Petit logo affiché pendant la vidéo (le grand logo de fin dépend de l'effet de fin). */
+ showWatermark?: boolean;
storeName?: string;
logoStart?: number | null;
fadeStart?: number | null;
@@ -28,7 +30,7 @@ export type ReelVideoProps = {
export const ReelVideo: React.FC = (props) => {
const { fps } = useVideoConfig();
const {
- videoUrl, videoDuration, totalDuration, words, captionStyle, logoUrl, storeName,
+ videoUrl, videoDuration, totalDuration, words, captionStyle, logoUrl, showWatermark = true, storeName,
logoStart = null, fadeStart = null, mixedAudioUrl, voiceUrl, voiceDelay = 2, musicUrl, musicVolume = 0.25,
} = props;
@@ -67,7 +69,7 @@ export const ReelVideo: React.FC = (props) => {
)}
- {logoUrl && }
+ {logoUrl && showWatermark && }
{logoStart != null && (logoUrl || storeName) && (
)}
diff --git a/ffmpeg-service/app/api.py b/ffmpeg-service/app/api.py
index 0aec2f1..1c57e5a 100644
--- a/ffmpeg-service/app/api.py
+++ b/ffmpeg-service/app/api.py
@@ -86,6 +86,8 @@ class ReelRequest(BaseModel):
# Préparation Remotion : le logo est composé par Remotion, mais sa présence
# allonge la vidéo pour laisser place à l'effet de fin.
has_logo: bool = False
+ # Petit logo en coin pendant la vidéo (le grand logo de fin dépend de enable_ending_effect)
+ show_watermark: bool = True
# Champs d'anciennes versions, acceptés et ignorés
music_id: str | None = None
word_duration: float | None = None
@@ -296,6 +298,7 @@ async def _gather(request: ReelRequest, workdir: Path, clock: Stopwatch, *, fetc
watermark=watermark if has_watermark else None,
outro_expected=not fetch_logo and request.has_logo,
ending_effect=request.enable_ending_effect,
+ show_watermark=request.show_watermark,
keep_original_audio=info.has_audio,
)
diff --git a/ffmpeg-service/app/render.py b/ffmpeg-service/app/render.py
index 79d6a05..89298d3 100644
--- a/ffmpeg-service/app/render.py
+++ b/ffmpeg-service/app/render.py
@@ -47,6 +47,8 @@ class RenderPlan:
# Effet de fin prévu sans que le logo passe par FFmpeg (rendu Remotion)
outro_expected: bool = False
ending_effect: bool = True
+ # Petit logo en coin pendant la vidéo (le grand logo de fin dépend de ending_effect)
+ show_watermark: bool = True
keep_original_audio: bool = False
@property
@@ -175,17 +177,25 @@ def build_command(plan: RenderPlan) -> list[str]:
current = "vbase"
if wm_idx is not None:
corner = "W-w-30:H-h-30"
- if plan.outro and plan.ending_effect:
- logo_start = plan.logo_start
- graph.append(f"[{wm_idx}:v]scale=200:-1,split=2[wm_small][wm_big0]")
- graph.append("[wm_big0]scale=-1:300[wm_big]")
- graph.append(f"[{current}][wm_small]overlay={corner}:enable='lt(t,{logo_start:.3f})'[vwm1]")
- graph.append(f"[vwm1][wm_big]overlay=(W-w)/2:(H-h)/2-100:enable='gte(t,{logo_start:.3f})'[vwm2]")
- current = "vwm2"
- else:
+ big_logo = bool(plan.outro and plan.ending_effect)
+ logo_start = plan.logo_start
+ if big_logo and plan.show_watermark:
+ graph.append(f"[{wm_idx}:v]split=2[wm_small0][wm_big0]")
+ graph.append("[wm_small0]scale=200:-1[wm_small]")
+ elif plan.show_watermark:
graph.append(f"[{wm_idx}:v]scale=200:-1[wm_small]")
- graph.append(f"[{current}][wm_small]overlay={corner}[vwm1]")
+ elif big_logo:
+ graph.append(f"[{wm_idx}:v]null[wm_big0]")
+ if plan.show_watermark:
+ until = f":enable='lt(t,{logo_start:.3f})'" if big_logo else ""
+ graph.append(f"[{current}][wm_small]overlay={corner}{until}[vwm1]")
current = "vwm1"
+ if big_logo:
+ graph.append("[wm_big0]scale=-1:300[wm_big]")
+ graph.append(
+ f"[{current}][wm_big]overlay=(W-w)/2:(H-h)/2-100:enable='gte(t,{logo_start:.3f})'[vwm2]"
+ )
+ current = "vwm2"
if plan.outro and plan.ending_effect:
graph.append(f"[{current}]subtitles='{filter_path(plan.outro)}'[vout0]")
current = "vout0"
diff --git a/ffmpeg-service/tests/test_render.py b/ffmpeg-service/tests/test_render.py
index 26b1911..56789fb 100644
--- a/ffmpeg-service/tests/test_render.py
+++ b/ffmpeg-service/tests/test_render.py
@@ -77,6 +77,16 @@ def test_big_logo_waits_for_the_end_of_the_voice():
assert "gte(t,6.800)" in _graph(build_command(plan))
+def test_corner_logo_can_be_hidden_while_keeping_the_ending_logo():
+ base = dict(video=Path("in.mp4"), video_duration=6.0, output=Path("out.mp4"), watermark=Path("logo.png"))
+ hidden = _graph(build_command(RenderPlan(**base, outro=Path("o.ass"), show_watermark=False)))
+ assert "W-w-30:H-h-30" not in hidden
+ assert "overlay=(W-w)/2" in hidden
+
+ nothing = _graph(build_command(RenderPlan(**base, show_watermark=False)))
+ assert "overlay" not in nothing
+
+
def test_prepared_video_has_no_audio_and_final_duration():
from app.render import build_prepared_video_command
diff --git a/server/routes/remotion.ts b/server/routes/remotion.ts
index 3187907..2be53f7 100644
--- a/server/routes/remotion.ts
+++ b/server/routes/remotion.ts
@@ -75,6 +75,7 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
ttsVoice: req.body.ttsVoice,
ttsStyle: req.body.ttsStyle || undefined,
captionStyle: req.body.captionStyle || undefined,
+ showLogo: req.body.showLogo !== "false",
storeName: await resolveStoreName(user.id, req.body.selectedPageId),
tempFiles: uploaded.map((f) => f.path),
});
diff --git a/server/services/ffmpeg.ts b/server/services/ffmpeg.ts
index 726248c..f007458 100644
--- a/server/services/ffmpeg.ts
+++ b/server/services/ffmpeg.ts
@@ -36,6 +36,8 @@ export interface ReelRenderOptions {
watermarkUrl?: string;
storeName?: string;
enableEndingEffect?: boolean;
+ /** Petit logo pendant la vidéo (le grand logo de fin dépend de enableEndingEffect). */
+ showLogo?: boolean;
}
export interface ReelRenderResult {
@@ -143,6 +145,7 @@ export class FFmpegService {
watermark_url: options.watermarkUrl,
store_name: options.storeName,
enable_ending_effect: options.enableEndingEffect ?? true,
+ show_watermark: options.showLogo ?? true,
};
console.log('🎬 Rendu du Reel :', {
diff --git a/server/services/reels/imagesPipeline.ts b/server/services/reels/imagesPipeline.ts
index 3c1bf0f..a38bc61 100644
--- a/server/services/reels/imagesPipeline.ts
+++ b/server/services/reels/imagesPipeline.ts
@@ -75,6 +75,7 @@ export async function runImagesReelJob({ job, progress }: JobContext): Promise {
await progress(15, "render");
+ // Le logo sert au coin de la vidéo et/ou à l'effet de fin : inutile de l'envoyer si aucun des deux
+ const logoUsed = Boolean(logoPath) && (options.showLogo !== false || options.enableEndingEffect !== false);
const rendered = await ffmpegService.renderReel(videoUrl, {
...options,
- watermarkUrl: logoPath ? resolveInternalUrl(logoPath) : undefined,
+ watermarkUrl: logoUsed && logoPath ? resolveInternalUrl(logoPath) : undefined,
});
return rendered.video;
}
@@ -133,6 +136,7 @@ async function renderWithRemotion(
words: prepared.words,
captionStyle: params.captionStyle,
logoUrl: logoPath ? await toDataUrl(logoPath) : undefined,
+ showWatermark: params.showLogo,
storeName: params.enableEndingEffect ? params.storeName : undefined,
logoStart: prepared.logoStart,
fadeStart: params.enableEndingEffect ? Math.max(0, prepared.totalDuration - FADE_SECONDS) : null,
diff --git a/shared/reel.test.ts b/shared/reel.test.ts
index f68500c..01b6c4e 100644
--- a/shared/reel.test.ts
+++ b/shared/reel.test.ts
@@ -10,7 +10,7 @@ describe("videoReelParamsSchema", () => {
it("applique les valeurs par défaut", () => {
const params = videoReelParamsSchema.parse({ videoMediaId: "v", pageIds: ["p"] });
- expect(params).toMatchObject({ captionStyle: "bold", stabilize: false, drawText: true, musicVolume: 0.25 });
+ expect(params).toMatchObject({ captionStyle: "bold", stabilize: false, drawText: true, musicVolume: 0.25, showLogo: true });
});
it("accepte les anciens paramètres encore stockés dans des jobs", () => {
diff --git a/shared/reel.ts b/shared/reel.ts
index 62cba2d..9e6e34b 100644
--- a/shared/reel.ts
+++ b/shared/reel.ts
@@ -39,6 +39,8 @@ export const videoReelParamsSchema = z.object({
drawText: z.boolean().default(true),
captionStyle: z.enum(CAPTION_STYLES).default(DEFAULT_CAPTION_STYLE),
stabilize: z.boolean().default(false),
+ // Petit logo pendant la vidéo ; le grand logo de fin dépend de enableEndingEffect
+ showLogo: z.boolean().default(true),
enableEndingEffect: z.boolean().default(true),
// Déterminé par le serveur à partir de la première page, jamais par le client
storeName: z.string().optional(),
@@ -58,6 +60,7 @@ export const imagesReelParamsSchema = z.object({
ttsVoice: optionalText,
ttsStyle: z.enum(TTS_STYLES).optional(),
captionStyle: z.enum(CAPTION_STYLES).default(DEFAULT_CAPTION_STYLE),
+ showLogo: z.boolean().default(true),
storeName: z.string().optional(),
// Fichiers temporaires à supprimer une fois le rendu terminé
tempFiles: z.array(z.string()).default([]),