mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-22 22:48:09 +02:00
feat(reels): optimize quality to 1080p, increase text size to 64, and add stabilization toggle
This commit is contained in:
1 parent
f95f400202
commit
7223bbbfb2
5 files changed
+51
-50
No files matched your search
@@ -76,6 +76,9 @@ export default function MobileNewReel() {
|
|||||||
// État audio preview
|
// État audio preview
|
||||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||||
const [musicOffset, setMusicOffset] = useState(0);
|
const [musicOffset, setMusicOffset] = useState(0);
|
||||||
|
const [stabilize, setStabilize] = useState(true); // Activé par défaut pour les Reels
|
||||||
|
const [force1080p, setForce1080p] = useState(true); // Activé par défaut
|
||||||
|
|
||||||
|
|
||||||
const { data: pages = [] } = useQuery<SocialPage[]>({
|
const { data: pages = [] } = useQuery<SocialPage[]>({
|
||||||
queryKey: ['/api/pages'],
|
queryKey: ['/api/pages'],
|
||||||
@@ -209,6 +212,7 @@ export default function MobileNewReel() {
|
|||||||
pageIds: selectedPages,
|
pageIds: selectedPages,
|
||||||
scheduledFor: scheduledDate?.toISOString(),
|
scheduledFor: scheduledDate?.toISOString(),
|
||||||
musicVolume: musicVolume[0] / 100,
|
musicVolume: musicVolume[0] / 100,
|
||||||
|
stabilize: stabilize,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,9 +297,27 @@ export default function MobileNewReel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedVideo && (
|
{selectedVideo && (
|
||||||
<Button className="w-full h-12 text-lg sticky bottom-4 shadow-xl" onClick={goNext}>
|
<div className="space-y-3">
|
||||||
Suivant <ChevronRight className="ml-2 w-5 h-5" />
|
<Card>
|
||||||
</Button>
|
<CardContent className="p-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Sparkles className="w-4 h-4 text-primary" /> Stabilisation & Qualité 1080p
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Recommandé pour Facebook Reels</div>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={stabilize}
|
||||||
|
onCheckedChange={(checked) => setStabilize(!!checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Button className="w-full h-12 text-lg sticky bottom-4 shadow-xl" onClick={goNext}>
|
||||||
|
Suivant <ChevronRight className="ml-2 w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+16
-43
@@ -31,7 +31,7 @@ class ReelRequest(BaseModel):
|
|||||||
music_id: Optional[str] = None
|
music_id: Optional[str] = None
|
||||||
music_url: Optional[str] = None
|
music_url: Optional[str] = None
|
||||||
word_duration: float = 0.6
|
word_duration: float = 0.6
|
||||||
font_size: int = 10
|
font_size: int = 64
|
||||||
music_volume: float = 0.25
|
music_volume: float = 0.25
|
||||||
tts_enabled: bool = False
|
tts_enabled: bool = False
|
||||||
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
||||||
@@ -359,7 +359,8 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
if has_tts:
|
if has_tts:
|
||||||
# Use subtitles filter
|
# Use subtitles filter
|
||||||
# Force style to look like TikTok/Reels text (No Black Box, just Outline)
|
# Force style to look like TikTok/Reels text (No Black Box, just Outline)
|
||||||
style = f"FontName=Arial,FontSize={request.font_size},PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=3,Shadow=0,Bold=1,Alignment=2,MarginV=150"
|
# Bigger font size (64 by default) and better vertical margin for 1080p
|
||||||
|
style = f"FontName=Arial,FontSize={request.font_size},PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=3,Shadow=0,Bold=1,Alignment=2,MarginV=300"
|
||||||
# Escape path for FFmpeg filter
|
# Escape path for FFmpeg filter
|
||||||
vtt_path_str = str(tts_vtt_path).replace("\\", "/").replace(":", "\\:")
|
vtt_path_str = str(tts_vtt_path).replace("\\", "/").replace(":", "\\:")
|
||||||
video_filters.append(
|
video_filters.append(
|
||||||
@@ -367,16 +368,12 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Standard Drawtext logic
|
# Standard Drawtext logic
|
||||||
|
# Scale font for 1080p
|
||||||
sanitized_text = request.text.replace("'", "").replace(":", "\\:")
|
sanitized_text = request.text.replace("'", "").replace(":", "\\:")
|
||||||
drawtext = f"drawtext=fontfile={FONT_PATH}:text='{sanitized_text}':fontcolor=white:fontsize={request.font_size}:borderw=3:bordercolor=black:x=(w-text_w)/2:y=h-text_h-150"
|
drawtext = f"drawtext=fontfile={FONT_PATH}:text='{sanitized_text}':fontcolor=white:fontsize={request.font_size}:borderw=3:bordercolor=black:x=(w-text_w)/2:y=h-text_h-300"
|
||||||
video_filters.append(drawtext)
|
video_filters.append(drawtext)
|
||||||
|
|
||||||
# Audio Mixing Strategy
|
# Audio Mixing Strategy
|
||||||
# We need to mix:
|
|
||||||
# 1. Original Video Audio (0:a) - if exists
|
|
||||||
# 2. Background Music (1:a) - if has_music
|
|
||||||
# 3. TTS Voice (1:a or 2:a) - if has_tts
|
|
||||||
|
|
||||||
# Detect if original video has audio
|
# Detect if original video has audio
|
||||||
has_original_audio = False
|
has_original_audio = False
|
||||||
try:
|
try:
|
||||||
@@ -400,67 +397,43 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
|
|
||||||
filter_complex_parts = []
|
filter_complex_parts = []
|
||||||
mix_inputs = 0
|
mix_inputs = 0
|
||||||
|
|
||||||
if has_music:
|
if has_music:
|
||||||
cmd.extend(["-i", str(input_audio_path)])
|
cmd.extend(["-i", str(input_audio_path)])
|
||||||
|
|
||||||
if has_tts:
|
if has_tts:
|
||||||
cmd.extend(["-i", str(tts_audio_path)])
|
cmd.extend(["-i", str(tts_audio_path)])
|
||||||
|
|
||||||
# Prepare inputs for mixing
|
|
||||||
# ALWAYS ignore original video audio - only use music and/or TTS
|
|
||||||
# (Original audio is never used per user request)
|
|
||||||
|
|
||||||
if has_music:
|
if has_music:
|
||||||
# Music volume - ensure it's properly scaled (0.0 to 1.0)
|
|
||||||
# Apply volume more aggressively for noticeable effect
|
|
||||||
music_vol = request.music_volume
|
music_vol = request.music_volume
|
||||||
if has_tts:
|
if has_tts:
|
||||||
# Reduce music even more when TTS is active so voice is clear
|
|
||||||
music_vol = music_vol * 0.3
|
music_vol = music_vol * 0.3
|
||||||
print(
|
|
||||||
f"🎵 Music volume applied: {music_vol:.2f} (requested: {request.music_volume})"
|
|
||||||
)
|
|
||||||
filter_complex_parts.append(f"[1:a]volume={music_vol}[a1]")
|
filter_complex_parts.append(f"[1:a]volume={music_vol}[a1]")
|
||||||
mix_inputs += 1
|
mix_inputs += 1
|
||||||
|
|
||||||
if has_tts:
|
if has_tts:
|
||||||
tts_idx = 2 if has_music else 1
|
tts_idx = 2 if has_music else 1
|
||||||
# Voice needs to be loud and clear
|
|
||||||
filter_complex_parts.append(f"[{tts_idx}:a]volume=1.5[a2]")
|
filter_complex_parts.append(f"[{tts_idx}:a]volume=1.5[a2]")
|
||||||
mix_inputs += 1
|
mix_inputs += 1
|
||||||
|
|
||||||
# Build mix command
|
|
||||||
if mix_inputs > 0:
|
if mix_inputs > 0:
|
||||||
inputs_str = ""
|
inputs_str = "".join(
|
||||||
# Note: Original audio is never mixed in (always muted)
|
["[a1]" if has_music else "", "[a2]" if has_tts else ""]
|
||||||
if has_music:
|
)
|
||||||
inputs_str += "[a1]"
|
|
||||||
if has_tts:
|
|
||||||
inputs_str += "[a2]"
|
|
||||||
|
|
||||||
# IMPORTANT: normalize=0 prevents amix from auto-adjusting volumes
|
|
||||||
# which was causing volume slider to have no effect
|
|
||||||
filter_complex_parts.append(
|
filter_complex_parts.append(
|
||||||
f"{inputs_str}amix=inputs={mix_inputs}:duration=first:dropout_transition=2:normalize=0[aout]"
|
f"{inputs_str}amix=inputs={mix_inputs}:duration=first:dropout_transition=2:normalize=0[aout]"
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.extend(["-filter_complex", ";".join(filter_complex_parts)])
|
cmd.extend(["-filter_complex", ";".join(filter_complex_parts)])
|
||||||
|
|
||||||
# Map processed video and audio
|
|
||||||
cmd.extend(["-map", "0:v", "-map", "[aout]"])
|
cmd.extend(["-map", "0:v", "-map", "[aout]"])
|
||||||
else:
|
else:
|
||||||
# No audio at all, just video
|
|
||||||
cmd.extend(["-map", "0:v"])
|
cmd.extend(["-map", "0:v"])
|
||||||
|
|
||||||
# Apply Video Filters if any
|
# Scaling to 1080x1920 (Vertical HD Reel Format)
|
||||||
|
# unsharp is added to counteract any blurring from scaling
|
||||||
|
format_filter = "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,unsharp=5:5:0.8:3:3:0.4"
|
||||||
|
video_filters.insert(0, format_filter)
|
||||||
|
|
||||||
if video_filters:
|
if video_filters:
|
||||||
cmd.extend(["-vf", ",".join(video_filters)])
|
cmd.extend(["-vf", ",".join(video_filters)])
|
||||||
|
|
||||||
# -shortest not needed with duration=first in amix, but good practice if logic changes
|
|
||||||
# actually duration=first in amix takes the length of the first input (usually video audio or music if mapped first)
|
|
||||||
# We want the video length to dictate.
|
|
||||||
# Easier: just use -shortest to cut audio to video length
|
|
||||||
cmd.extend(["-shortest"])
|
cmd.extend(["-shortest"])
|
||||||
|
|
||||||
# Quality settings
|
# Quality settings
|
||||||
@@ -469,15 +442,15 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
"-c:v",
|
"-c:v",
|
||||||
"libx264",
|
"libx264",
|
||||||
"-preset",
|
"-preset",
|
||||||
"medium",
|
"slow",
|
||||||
"-crf",
|
"-crf",
|
||||||
"18", # High quality output (lower = better, 18 is visually lossless)
|
"17",
|
||||||
"-c:a",
|
"-c:a",
|
||||||
"aac",
|
"aac",
|
||||||
"-b:a",
|
"-b:a",
|
||||||
"192k",
|
"192k",
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p", # Ensure compatibility
|
"yuv420p",
|
||||||
"-movflags",
|
"-movflags",
|
||||||
"+faststart",
|
"+faststart",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
ttsVoice,
|
ttsVoice,
|
||||||
wordDuration = 0.6,
|
wordDuration = 0.6,
|
||||||
fontSize = 16,
|
fontSize = 64,
|
||||||
musicVolume = 0.25,
|
musicVolume = 0.25,
|
||||||
drawText = true,
|
drawText = true,
|
||||||
stabilize = false,
|
stabilize = false,
|
||||||
@@ -281,7 +281,7 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
|
|||||||
pageIds,
|
pageIds,
|
||||||
scheduledFor,
|
scheduledFor,
|
||||||
wordDuration = 0.6,
|
wordDuration = 0.6,
|
||||||
fontSize = 16,
|
fontSize = 64,
|
||||||
musicVolume = 0.25,
|
musicVolume = 0.25,
|
||||||
drawText = true,
|
drawText = true,
|
||||||
stabilize = false,
|
stabilize = false,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export class FFmpegService {
|
|||||||
tts_enabled: options.ttsEnabled,
|
tts_enabled: options.ttsEnabled,
|
||||||
tts_voice: options.ttsVoice,
|
tts_voice: options.ttsVoice,
|
||||||
word_duration: options.wordDuration ?? 0.6,
|
word_duration: options.wordDuration ?? 0.6,
|
||||||
font_size: options.fontSize ?? 16,
|
font_size: options.fontSize ?? 64,
|
||||||
music_volume: options.musicVolume ?? 0.25,
|
music_volume: options.musicVolume ?? 0.25,
|
||||||
draw_text: options.drawText ?? true,
|
draw_text: options.drawText ?? true,
|
||||||
stabilize: options.stabilize ?? false,
|
stabilize: options.stabilize ?? false,
|
||||||
@@ -180,7 +180,7 @@ export class FFmpegService {
|
|||||||
tts_enabled: options.ttsEnabled,
|
tts_enabled: options.ttsEnabled,
|
||||||
tts_voice: options.ttsVoice,
|
tts_voice: options.ttsVoice,
|
||||||
word_duration: options.wordDuration ?? 0.6,
|
word_duration: options.wordDuration ?? 0.6,
|
||||||
font_size: options.fontSize ?? 16,
|
font_size: options.fontSize ?? 64,
|
||||||
music_volume: options.musicVolume ?? 0.25,
|
music_volume: options.musicVolume ?? 0.25,
|
||||||
draw_text: options.drawText ?? true,
|
draw_text: options.drawText ?? true,
|
||||||
stabilize: options.stabilize ?? false,
|
stabilize: options.stabilize ?? false,
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ Ajout d'une fonctionnalité complète de création de Reels Facebook permettant
|
|||||||
- [x] **Fix API 500** (`server/routes/reels.ts`) : Variable `stabilize` manquante corrigée.
|
- [x] **Fix API 500** (`server/routes/reels.ts`) : Variable `stabilize` manquante corrigée.
|
||||||
- [x] **Fix iOS Upload** (`client/src/pages/new-reel.tsx`) : Ajout support `.mov` et augmentation limite taille (500MB).
|
- [x] **Fix iOS Upload** (`client/src/pages/new-reel.tsx`) : Ajout support `.mov` et augmentation limite taille (500MB).
|
||||||
- [ ] **Support Gros Fichiers (4GB)** : Passage à DiskStorage et streaming Cloudinary.
|
- [ ] **Support Gros Fichiers (4GB)** : Passage à DiskStorage et streaming Cloudinary.
|
||||||
|
- [ ] **Phase 10: Optimisation 1080p & Qualité Maximale** ⏳
|
||||||
|
- [x] FFmpeg: Forcer scaling 1080x1920 (Vertical HD)
|
||||||
|
- [x] FFmpeg: Augmenter CRF à 17 et preset à `slow`
|
||||||
|
- [x] FFmpeg: Augmenter taille police par défaut à 64
|
||||||
|
- [x] FFmpeg: Positionner texte plus bas (proportionnel 1080p)
|
||||||
|
- [/] Frontend: Ajouter toggle Stabilisation et Qualité
|
||||||
|
|
||||||
## Progress Log
|
## Progress Log
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user