mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 06:58:12 +02:00
feat(reels): interrupteur « Afficher le logo sur la vidéo »
Le petit logo affiché pendant la vidéo devient optionnel (activé par défaut), sur les 4 pages Reel (vidéo et images, bureau et mobile) ainsi que dans l'aperçu. Le grand logo de l'effet de fin reste piloté par son propre interrupteur. Pris en charge par le rendu Remotion et par le rendu FFmpeg de secours (show_watermark). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
This commit is contained in:
16 files changed
+102
-17
No files matched your search
@@ -32,6 +32,8 @@ interface CommonProps {
|
|||||||
musicUrl?: string;
|
musicUrl?: string;
|
||||||
musicVolume: number;
|
musicVolume: number;
|
||||||
logoUrl?: string | null;
|
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;
|
storeName?: string;
|
||||||
endingEffect: boolean;
|
endingEffect: boolean;
|
||||||
/** Sous-titres SRT : remplacent `text`, minutés par le fichier. */
|
/** Sous-titres SRT : remplacent `text`, minutés par le fichier. */
|
||||||
@@ -74,7 +76,8 @@ function estimatedVoiceDuration(text: string): number {
|
|||||||
*/
|
*/
|
||||||
export function ReelPreview(props: ReelPreviewProps) {
|
export function ReelPreview(props: ReelPreviewProps) {
|
||||||
const videoDuration = useVideoDuration(props.kind === "video" ? props.videoUrl : undefined);
|
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 logoUrl = props.logoUrl ?? undefined;
|
||||||
const storeName = endingEffect ? props.storeName || undefined : undefined;
|
const storeName = endingEffect ? props.storeName || undefined : undefined;
|
||||||
const srtCues = props.kind === "video" && props.srtCues?.length ? props.srtCues : null;
|
const srtCues = props.kind === "video" && props.srtCues?.length ? props.srtCues : null;
|
||||||
@@ -106,6 +109,7 @@ export function ReelPreview(props: ReelPreviewProps) {
|
|||||||
musicUrl,
|
musicUrl,
|
||||||
musicVolume,
|
musicVolume,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
|
showWatermark,
|
||||||
storeName,
|
storeName,
|
||||||
endingSeconds,
|
endingSeconds,
|
||||||
};
|
};
|
||||||
@@ -136,6 +140,7 @@ export function ReelPreview(props: ReelPreviewProps) {
|
|||||||
words,
|
words,
|
||||||
captionStyle,
|
captionStyle,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
|
showWatermark,
|
||||||
storeName,
|
storeName,
|
||||||
logoStart: timing.logoStart,
|
logoStart: timing.logoStart,
|
||||||
fadeStart: timing.fadeStart,
|
fadeStart: timing.fadeStart,
|
||||||
@@ -150,7 +155,7 @@ export function ReelPreview(props: ReelPreviewProps) {
|
|||||||
props.kind,
|
props.kind,
|
||||||
props.kind === "video" ? props.videoUrl : props.images.join("|"),
|
props.kind === "video" ? props.videoUrl : props.images.join("|"),
|
||||||
videoDuration, text, cleanText, srtCues, showCaptions, captionStyle, ttsEnabled, voice, voiceDuration,
|
videoDuration, text, cleanText, srtCues, showCaptions, captionStyle, ttsEnabled, voice, voiceDuration,
|
||||||
musicUrl, musicVolume, logoUrl, storeName, endingEffect,
|
musicUrl, musicVolume, logoUrl, showWatermark, storeName, endingEffect,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!composition) {
|
if (!composition) {
|
||||||
|
|||||||
@@ -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
|
// 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 [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
||||||
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
|
|
||||||
// TTS Sync state
|
// TTS Sync state
|
||||||
const [syncInfo, setSyncInfo] = useState<{
|
const [syncInfo, setSyncInfo] = useState<{
|
||||||
@@ -280,6 +281,7 @@ export default function MobileNewReel() {
|
|||||||
ttsStyle,
|
ttsStyle,
|
||||||
captionStyle,
|
captionStyle,
|
||||||
enableEndingEffect,
|
enableEndingEffect,
|
||||||
|
showLogo,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -484,6 +486,16 @@ export default function MobileNewReel() {
|
|||||||
<Label className="text-xs font-medium">Style des sous-titres</Label>
|
<Label className="text-xs font-medium">Style des sous-titres</Label>
|
||||||
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} compact />
|
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} compact />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 mt-4">
|
||||||
|
<Switch
|
||||||
|
id="show-logo"
|
||||||
|
checked={showLogo}
|
||||||
|
onCheckedChange={setShowLogo}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="show-logo" className="font-medium cursor-pointer">
|
||||||
|
Afficher le logo sur la vidéo
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
<div className="flex items-center space-x-2 mt-4">
|
<div className="flex items-center space-x-2 mt-4">
|
||||||
<Switch
|
<Switch
|
||||||
id="enable-ending-effect"
|
id="enable-ending-effect"
|
||||||
@@ -572,6 +584,7 @@ export default function MobileNewReel() {
|
|||||||
musicUrl={selectedTrack?.previewUrl}
|
musicUrl={selectedTrack?.previewUrl}
|
||||||
musicVolume={musicVolume[0] / 100}
|
musicVolume={musicVolume[0] / 100}
|
||||||
logoUrl={reelConfig?.logoUrl}
|
logoUrl={reelConfig?.logoUrl}
|
||||||
|
showWatermark={showLogo}
|
||||||
storeName={pages.find((p) => p.id === selectedPages[0])?.pageName}
|
storeName={pages.find((p) => p.id === selectedPages[0])?.pageName}
|
||||||
endingEffect={enableEndingEffect}
|
endingEffect={enableEndingEffect}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function MobileRemotionVideoPage() {
|
|||||||
const [publishDescription, setPublishDescription] = useState("");
|
const [publishDescription, setPublishDescription] = useState("");
|
||||||
const [isPublishing, setIsPublishing] = useState(false);
|
const [isPublishing, setIsPublishing] = useState(false);
|
||||||
const [captionStyle, setCaptionStyle] = useState<CaptionStyle>(DEFAULT_CAPTION_STYLE);
|
const [captionStyle, setCaptionStyle] = useState<CaptionStyle>(DEFAULT_CAPTION_STYLE);
|
||||||
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
const [voicePreview, setVoicePreview] = useState<VoicePreviewResult | null>(null);
|
const [voicePreview, setVoicePreview] = useState<VoicePreviewResult | null>(null);
|
||||||
const [renderProgress, setRenderProgress] = useState(0);
|
const [renderProgress, setRenderProgress] = useState(0);
|
||||||
const currentVoice = isVoicePreviewCurrent(voicePreview, overlayText, voiceSettings) ? voicePreview : null;
|
const currentVoice = isVoicePreviewCurrent(voicePreview, overlayText, voiceSettings) ? voicePreview : null;
|
||||||
@@ -169,6 +170,7 @@ export default function MobileRemotionVideoPage() {
|
|||||||
formData.append("ttsVoice", voiceSettings.voice);
|
formData.append("ttsVoice", voiceSettings.voice);
|
||||||
formData.append("ttsStyle", voiceSettings.style);
|
formData.append("ttsStyle", voiceSettings.style);
|
||||||
formData.append("captionStyle", captionStyle);
|
formData.append("captionStyle", captionStyle);
|
||||||
|
formData.append("showLogo", String(showLogo));
|
||||||
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
||||||
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
||||||
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); 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() {
|
|||||||
<Label className="text-xs font-medium">Style des sous-titres</Label>
|
<Label className="text-xs font-medium">Style des sous-titres</Label>
|
||||||
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} compact />
|
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} compact />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Switch id="show-logo-m" checked={showLogo} onCheckedChange={setShowLogo} />
|
||||||
|
<Label htmlFor="show-logo-m" className="text-sm">Afficher le logo sur la vidéo</Label>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch id="tts-m" checked={ttsEnabled} onCheckedChange={setTtsEnabled} />
|
<Switch id="tts-m" checked={ttsEnabled} onCheckedChange={setTtsEnabled} />
|
||||||
<Label htmlFor="tts-m" className="text-sm">Voix TTS</Label>
|
<Label htmlFor="tts-m" className="text-sm">Voix TTS</Label>
|
||||||
@@ -474,6 +480,7 @@ export default function MobileRemotionVideoPage() {
|
|||||||
musicUrl={musicFileUrl ?? selectedTrack?.url}
|
musicUrl={musicFileUrl ?? selectedTrack?.url}
|
||||||
musicVolume={musicVolume}
|
musicVolume={musicVolume}
|
||||||
logoUrl={reelConfig?.logoUrl}
|
logoUrl={reelConfig?.logoUrl}
|
||||||
|
showWatermark={showLogo}
|
||||||
storeName={socialPages.find((p) => p.id === selectedPageIds[0])?.pageName ?? socialPages[0]?.pageName}
|
storeName={socialPages.find((p) => p.id === selectedPageIds[0])?.pageName ?? socialPages[0]?.pageName}
|
||||||
endingEffect
|
endingEffect
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
// 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 [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
||||||
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
|
|
||||||
// TTS Sync state
|
// TTS Sync state
|
||||||
const [syncInfo, setSyncInfo] = useState<{
|
const [syncInfo, setSyncInfo] = useState<{
|
||||||
@@ -423,6 +424,7 @@ export default function NewReel() {
|
|||||||
drawText,
|
drawText,
|
||||||
stabilize: stabilize,
|
stabilize: stabilize,
|
||||||
enableEndingEffect,
|
enableEndingEffect,
|
||||||
|
showLogo,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -818,6 +820,17 @@ export default function NewReel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 mt-4">
|
||||||
|
<Switch
|
||||||
|
id="show-logo"
|
||||||
|
checked={showLogo}
|
||||||
|
onCheckedChange={setShowLogo}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="show-logo" className="font-medium cursor-pointer">
|
||||||
|
Afficher le logo sur la vidéo
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 mt-4">
|
<div className="flex items-center space-x-2 mt-4">
|
||||||
<Switch
|
<Switch
|
||||||
id="enable-ending-effect"
|
id="enable-ending-effect"
|
||||||
@@ -1012,6 +1025,7 @@ export default function NewReel() {
|
|||||||
musicUrl={selectedTrack?.previewUrl}
|
musicUrl={selectedTrack?.previewUrl}
|
||||||
musicVolume={musicVolume[0] / 100}
|
musicVolume={musicVolume[0] / 100}
|
||||||
logoUrl={reelConfig?.logoUrl}
|
logoUrl={reelConfig?.logoUrl}
|
||||||
|
showWatermark={showLogo}
|
||||||
storeName={pages.find((p) => p.id === selectedPages[0])?.pageName}
|
storeName={pages.find((p) => p.id === selectedPages[0])?.pageName}
|
||||||
endingEffect={enableEndingEffect}
|
endingEffect={enableEndingEffect}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default function RemotionVideoPage() {
|
|||||||
const [publishDescription, setPublishDescription] = useState("");
|
const [publishDescription, setPublishDescription] = useState("");
|
||||||
const [isPublishing, setIsPublishing] = useState(false);
|
const [isPublishing, setIsPublishing] = useState(false);
|
||||||
const [captionStyle, setCaptionStyle] = useState<CaptionStyle>(DEFAULT_CAPTION_STYLE);
|
const [captionStyle, setCaptionStyle] = useState<CaptionStyle>(DEFAULT_CAPTION_STYLE);
|
||||||
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
const [voicePreview, setVoicePreview] = useState<VoicePreviewResult | null>(null);
|
const [voicePreview, setVoicePreview] = useState<VoicePreviewResult | null>(null);
|
||||||
const [renderProgress, setRenderProgress] = useState(0);
|
const [renderProgress, setRenderProgress] = useState(0);
|
||||||
const currentVoice = isVoicePreviewCurrent(voicePreview, overlayText, voiceSettings) ? voicePreview : null;
|
const currentVoice = isVoicePreviewCurrent(voicePreview, overlayText, voiceSettings) ? voicePreview : null;
|
||||||
@@ -135,6 +136,7 @@ export default function RemotionVideoPage() {
|
|||||||
formData.append("ttsVoice", voiceSettings.voice);
|
formData.append("ttsVoice", voiceSettings.voice);
|
||||||
formData.append("ttsStyle", voiceSettings.style);
|
formData.append("ttsStyle", voiceSettings.style);
|
||||||
formData.append("captionStyle", captionStyle);
|
formData.append("captionStyle", captionStyle);
|
||||||
|
formData.append("showLogo", String(showLogo));
|
||||||
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
||||||
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
||||||
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); 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() {
|
|||||||
<Label className="text-sm font-medium">Style des sous-titres</Label>
|
<Label className="text-sm font-medium">Style des sous-titres</Label>
|
||||||
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} />
|
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Switch id="show-logo" checked={showLogo} onCheckedChange={setShowLogo} />
|
||||||
|
<Label htmlFor="show-logo">Afficher le logo sur la vidéo</Label>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch id="tts" checked={ttsEnabled} onCheckedChange={setTtsEnabled} />
|
<Switch id="tts" checked={ttsEnabled} onCheckedChange={setTtsEnabled} />
|
||||||
<Label htmlFor="tts">Activer la voix TTS</Label>
|
<Label htmlFor="tts">Activer la voix TTS</Label>
|
||||||
@@ -420,6 +426,7 @@ export default function RemotionVideoPage() {
|
|||||||
musicUrl={musicFileUrl ?? selectedTrack?.url}
|
musicUrl={musicFileUrl ?? selectedTrack?.url}
|
||||||
musicVolume={musicVolume}
|
musicVolume={musicVolume}
|
||||||
logoUrl={reelConfig?.logoUrl}
|
logoUrl={reelConfig?.logoUrl}
|
||||||
|
showWatermark={showLogo}
|
||||||
storeName={socialPages.find((p) => p.id === selectedPageIds[0])?.pageName ?? socialPages[0]?.pageName}
|
storeName={socialPages.find((p) => p.id === selectedPageIds[0])?.pageName ?? socialPages[0]?.pageName}
|
||||||
endingEffect
|
endingEffect
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export type ImageCompositionProps = {
|
|||||||
musicUrl?: string;
|
musicUrl?: string;
|
||||||
musicVolume?: number;
|
musicVolume?: number;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
/** Petit logo affiché pendant la vidéo (le grand logo de fin reste sur la diapositive de fin). */
|
||||||
|
showWatermark?: boolean;
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
/** Durée de la diapositive de fin (logo + nom du magasin), en secondes. */
|
/** Durée de la diapositive de fin (logo + nom du magasin), en secondes. */
|
||||||
endingSeconds?: number;
|
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. */
|
/** Reel à partir d'images : diaporama animé, voix, sous-titres et diapositive de fin. */
|
||||||
export const ImageComposition: React.FC<ImageCompositionProps> = ({
|
export const ImageComposition: React.FC<ImageCompositionProps> = ({
|
||||||
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 { fps, durationInFrames } = useVideoConfig();
|
||||||
const hasEnding = Boolean(logoUrl || storeName) && endingSeconds > 0;
|
const hasEnding = Boolean(logoUrl || storeName) && endingSeconds > 0;
|
||||||
@@ -113,7 +115,7 @@ export const ImageComposition: React.FC<ImageCompositionProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Captions words={words} style={captionStyle} hideAfter={hasEnding ? endingStart : null} />
|
<Captions words={words} style={captionStyle} hideAfter={hasEnding ? endingStart : null} />
|
||||||
{logoUrl && <Watermark logoUrl={logoUrl} until={hasEnding ? endingStart : null} />}
|
{logoUrl && showWatermark && <Watermark logoUrl={logoUrl} until={hasEnding ? endingStart : null} />}
|
||||||
{hasEnding && <Outro start={endingStart} logoUrl={logoUrl} storeName={storeName} opaque />}
|
{hasEnding && <Outro start={endingStart} logoUrl={logoUrl} storeName={storeName} opaque />}
|
||||||
</AbsoluteFill>
|
</AbsoluteFill>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type ReelVideoProps = {
|
|||||||
words: TimedWord[];
|
words: TimedWord[];
|
||||||
captionStyle: CaptionStyle;
|
captionStyle: CaptionStyle;
|
||||||
logoUrl?: string;
|
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;
|
storeName?: string;
|
||||||
logoStart?: number | null;
|
logoStart?: number | null;
|
||||||
fadeStart?: number | null;
|
fadeStart?: number | null;
|
||||||
@@ -28,7 +30,7 @@ export type ReelVideoProps = {
|
|||||||
export const ReelVideo: React.FC<ReelVideoProps> = (props) => {
|
export const ReelVideo: React.FC<ReelVideoProps> = (props) => {
|
||||||
const { fps } = useVideoConfig();
|
const { fps } = useVideoConfig();
|
||||||
const {
|
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,
|
logoStart = null, fadeStart = null, mixedAudioUrl, voiceUrl, voiceDelay = 2, musicUrl, musicVolume = 0.25,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ export const ReelVideo: React.FC<ReelVideoProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Captions words={words} style={captionStyle} hideAfter={logoStart} />
|
<Captions words={words} style={captionStyle} hideAfter={logoStart} />
|
||||||
{logoUrl && <Watermark logoUrl={logoUrl} until={logoStart} />}
|
{logoUrl && showWatermark && <Watermark logoUrl={logoUrl} until={logoStart} />}
|
||||||
{logoStart != null && (logoUrl || storeName) && (
|
{logoStart != null && (logoUrl || storeName) && (
|
||||||
<Outro start={logoStart} logoUrl={logoUrl} storeName={storeName} />
|
<Outro start={logoStart} logoUrl={logoUrl} storeName={storeName} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class ReelRequest(BaseModel):
|
|||||||
# Préparation Remotion : le logo est composé par Remotion, mais sa présence
|
# Préparation Remotion : le logo est composé par Remotion, mais sa présence
|
||||||
# allonge la vidéo pour laisser place à l'effet de fin.
|
# allonge la vidéo pour laisser place à l'effet de fin.
|
||||||
has_logo: bool = False
|
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
|
# Champs d'anciennes versions, acceptés et ignorés
|
||||||
music_id: str | None = None
|
music_id: str | None = None
|
||||||
word_duration: float | 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,
|
watermark=watermark if has_watermark else None,
|
||||||
outro_expected=not fetch_logo and request.has_logo,
|
outro_expected=not fetch_logo and request.has_logo,
|
||||||
ending_effect=request.enable_ending_effect,
|
ending_effect=request.enable_ending_effect,
|
||||||
|
show_watermark=request.show_watermark,
|
||||||
keep_original_audio=info.has_audio,
|
keep_original_audio=info.has_audio,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class RenderPlan:
|
|||||||
# Effet de fin prévu sans que le logo passe par FFmpeg (rendu Remotion)
|
# Effet de fin prévu sans que le logo passe par FFmpeg (rendu Remotion)
|
||||||
outro_expected: bool = False
|
outro_expected: bool = False
|
||||||
ending_effect: bool = True
|
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
|
keep_original_audio: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -175,17 +177,25 @@ def build_command(plan: RenderPlan) -> list[str]:
|
|||||||
current = "vbase"
|
current = "vbase"
|
||||||
if wm_idx is not None:
|
if wm_idx is not None:
|
||||||
corner = "W-w-30:H-h-30"
|
corner = "W-w-30:H-h-30"
|
||||||
if plan.outro and plan.ending_effect:
|
big_logo = bool(plan.outro and plan.ending_effect)
|
||||||
logo_start = plan.logo_start
|
logo_start = plan.logo_start
|
||||||
graph.append(f"[{wm_idx}:v]scale=200:-1,split=2[wm_small][wm_big0]")
|
if big_logo and plan.show_watermark:
|
||||||
graph.append("[wm_big0]scale=-1:300[wm_big]")
|
graph.append(f"[{wm_idx}:v]split=2[wm_small0][wm_big0]")
|
||||||
graph.append(f"[{current}][wm_small]overlay={corner}:enable='lt(t,{logo_start:.3f})'[vwm1]")
|
graph.append("[wm_small0]scale=200:-1[wm_small]")
|
||||||
graph.append(f"[vwm1][wm_big]overlay=(W-w)/2:(H-h)/2-100:enable='gte(t,{logo_start:.3f})'[vwm2]")
|
elif plan.show_watermark:
|
||||||
current = "vwm2"
|
|
||||||
else:
|
|
||||||
graph.append(f"[{wm_idx}:v]scale=200:-1[wm_small]")
|
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"
|
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:
|
if plan.outro and plan.ending_effect:
|
||||||
graph.append(f"[{current}]subtitles='{filter_path(plan.outro)}'[vout0]")
|
graph.append(f"[{current}]subtitles='{filter_path(plan.outro)}'[vout0]")
|
||||||
current = "vout0"
|
current = "vout0"
|
||||||
|
|||||||
@@ -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))
|
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():
|
def test_prepared_video_has_no_audio_and_final_duration():
|
||||||
from app.render import build_prepared_video_command
|
from app.render import build_prepared_video_command
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
|
|||||||
ttsVoice: req.body.ttsVoice,
|
ttsVoice: req.body.ttsVoice,
|
||||||
ttsStyle: req.body.ttsStyle || undefined,
|
ttsStyle: req.body.ttsStyle || undefined,
|
||||||
captionStyle: req.body.captionStyle || undefined,
|
captionStyle: req.body.captionStyle || undefined,
|
||||||
|
showLogo: req.body.showLogo !== "false",
|
||||||
storeName: await resolveStoreName(user.id, req.body.selectedPageId),
|
storeName: await resolveStoreName(user.id, req.body.selectedPageId),
|
||||||
tempFiles: uploaded.map((f) => f.path),
|
tempFiles: uploaded.map((f) => f.path),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface ReelRenderOptions {
|
|||||||
watermarkUrl?: string;
|
watermarkUrl?: string;
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
enableEndingEffect?: boolean;
|
enableEndingEffect?: boolean;
|
||||||
|
/** Petit logo pendant la vidéo (le grand logo de fin dépend de enableEndingEffect). */
|
||||||
|
showLogo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReelRenderResult {
|
export interface ReelRenderResult {
|
||||||
@@ -143,6 +145,7 @@ export class FFmpegService {
|
|||||||
watermark_url: options.watermarkUrl,
|
watermark_url: options.watermarkUrl,
|
||||||
store_name: options.storeName,
|
store_name: options.storeName,
|
||||||
enable_ending_effect: options.enableEndingEffect ?? true,
|
enable_ending_effect: options.enableEndingEffect ?? true,
|
||||||
|
show_watermark: options.showLogo ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('🎬 Rendu du Reel :', {
|
console.log('🎬 Rendu du Reel :', {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export async function runImagesReelJob({ job, progress }: JobContext): Promise<I
|
|||||||
musicUrl,
|
musicUrl,
|
||||||
musicVolume: params.musicVolume,
|
musicVolume: params.musicVolume,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
|
showWatermark: params.showLogo,
|
||||||
storeName,
|
storeName,
|
||||||
endingSeconds,
|
endingSeconds,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export async function runVideoReelJob({ job, progress }: JobContext) {
|
|||||||
stabilize: params.stabilize,
|
stabilize: params.stabilize,
|
||||||
storeName: params.storeName,
|
storeName: params.storeName,
|
||||||
enableEndingEffect: params.enableEndingEffect,
|
enableEndingEffect: params.enableEndingEffect,
|
||||||
|
showLogo: params.showLogo,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = configuredRenderer();
|
const renderer = configuredRenderer();
|
||||||
@@ -98,9 +99,11 @@ async function renderWithFfmpeg(
|
|||||||
progress: JobContext["progress"],
|
progress: JobContext["progress"],
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
await progress(15, "render");
|
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, {
|
const rendered = await ffmpegService.renderReel(videoUrl, {
|
||||||
...options,
|
...options,
|
||||||
watermarkUrl: logoPath ? resolveInternalUrl(logoPath) : undefined,
|
watermarkUrl: logoUsed && logoPath ? resolveInternalUrl(logoPath) : undefined,
|
||||||
});
|
});
|
||||||
return rendered.video;
|
return rendered.video;
|
||||||
}
|
}
|
||||||
@@ -133,6 +136,7 @@ async function renderWithRemotion(
|
|||||||
words: prepared.words,
|
words: prepared.words,
|
||||||
captionStyle: params.captionStyle,
|
captionStyle: params.captionStyle,
|
||||||
logoUrl: logoPath ? await toDataUrl(logoPath) : undefined,
|
logoUrl: logoPath ? await toDataUrl(logoPath) : undefined,
|
||||||
|
showWatermark: params.showLogo,
|
||||||
storeName: params.enableEndingEffect ? params.storeName : undefined,
|
storeName: params.enableEndingEffect ? params.storeName : undefined,
|
||||||
logoStart: prepared.logoStart,
|
logoStart: prepared.logoStart,
|
||||||
fadeStart: params.enableEndingEffect ? Math.max(0, prepared.totalDuration - FADE_SECONDS) : null,
|
fadeStart: params.enableEndingEffect ? Math.max(0, prepared.totalDuration - FADE_SECONDS) : null,
|
||||||
|
|||||||
+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 });
|
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", () => {
|
it("accepte les anciens paramètres encore stockés dans des jobs", () => {
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const videoReelParamsSchema = z.object({
|
|||||||
drawText: z.boolean().default(true),
|
drawText: z.boolean().default(true),
|
||||||
captionStyle: z.enum(CAPTION_STYLES).default(DEFAULT_CAPTION_STYLE),
|
captionStyle: z.enum(CAPTION_STYLES).default(DEFAULT_CAPTION_STYLE),
|
||||||
stabilize: z.boolean().default(false),
|
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),
|
enableEndingEffect: z.boolean().default(true),
|
||||||
// Déterminé par le serveur à partir de la première page, jamais par le client
|
// Déterminé par le serveur à partir de la première page, jamais par le client
|
||||||
storeName: z.string().optional(),
|
storeName: z.string().optional(),
|
||||||
@@ -58,6 +60,7 @@ export const imagesReelParamsSchema = z.object({
|
|||||||
ttsVoice: optionalText,
|
ttsVoice: optionalText,
|
||||||
ttsStyle: z.enum(TTS_STYLES).optional(),
|
ttsStyle: z.enum(TTS_STYLES).optional(),
|
||||||
captionStyle: z.enum(CAPTION_STYLES).default(DEFAULT_CAPTION_STYLE),
|
captionStyle: z.enum(CAPTION_STYLES).default(DEFAULT_CAPTION_STYLE),
|
||||||
|
showLogo: z.boolean().default(true),
|
||||||
storeName: z.string().optional(),
|
storeName: z.string().optional(),
|
||||||
// Fichiers temporaires à supprimer une fois le rendu terminé
|
// Fichiers temporaires à supprimer une fois le rendu terminé
|
||||||
tempFiles: z.array(z.string()).default([]),
|
tempFiles: z.array(z.string()).default([]),
|
||||||
|
|||||||
Reference in new issue
Block a user