import { useState, useRef } from "react"; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import { useToast } from "@/hooks/use-toast"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { UploadCloud, Video, Loader2, Check, Sparkles, Mic, Volume2, Music, Play, Pause, Send } from "lucide-react"; import { Slider } from "@/components/ui/slider"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Checkbox } from "@/components/ui/checkbox"; import { DateTimePicker } from "@/components/datetime-picker"; import type { Media, SocialPage } from "@shared/schema"; interface AudioTrack { id: string; title: string; fileName: string; url: string; duration: number; } export default function RemotionVideoPage() { const [sidebarOpen, setSidebarOpen] = useState(false); const [images, setImages] = useState([]); const [selectedLibraryImages, setSelectedLibraryImages] = useState([]); const [overlayText, setOverlayText] = useState(""); const [productInfo, setProductInfo] = useState(""); const [generatedVariants, setGeneratedVariants] = useState([]); const [ttsEnabled, setTtsEnabled] = useState(true); const [ttsEngine, setTtsEngine] = useState<'edge' | 'gemini'>('edge'); const [ttsVoice, setTtsVoice] = useState('fr-FR-VivienneMultilingualNeural'); // Gemini native TTS voices (Charon = homme, Kore = femme) const geminiVoices = [ { label: 'Charon - Voix Homme', value: 'fr-FR-Standard-B' }, { label: 'Kore - Voix Femme', value: 'fr-FR-Standard-A' }, ]; // French Edge TTS voices const edgeVoices = [ { label: 'Vivienne (Femme)', value: 'fr-FR-VivienneMultilingualNeural' }, { label: 'Henri (Homme)', value: 'fr-FR-HenriNeural' }, { label: 'Denise (Femme)', value: 'fr-FR-DeniseNeural' }, { label: 'Rémy (Homme)', value: 'fr-FR-RemyMultilingualNeural' }, { label: 'Jenny (Anglaise, Femme)', value: 'en-US-JennyNeural' }, ]; const [musicFile, setMusicFile] = useState(null); const [selectedTrack, setSelectedTrack] = useState(null); const [musicVolume, setMusicVolume] = useState(0.3); const [isPlaying, setIsPlaying] = useState(null); const [isRendering, setIsRendering] = useState(false); const [videoUrl, setVideoUrl] = useState(null); const [selectedPageIds, setSelectedPageIds] = useState([]); const [scheduledDate, setScheduledDate] = useState(undefined); const [publishDescription, setPublishDescription] = useState(""); const [isPublishing, setIsPublishing] = useState(false); const audioRef = useRef(null); const { toast } = useToast(); const { data: allMedia = [] } = useQuery({ queryKey: ['/api/media'] }); const { data: audioTracks = [], isLoading: tracksLoading } = useQuery({ queryKey: ['/api/audio-tracks'] }); const { data: socialPages = [] } = useQuery({ queryKey: ['/api/pages'] }); const facebookPages = socialPages.filter(p => p.platform === 'facebook'); const imageMediaList = allMedia.filter(m => m.type === 'image').slice(0, 20); const totalSelected = images.length + selectedLibraryImages.length; const generateTextMutation = useMutation({ mutationFn: async (text: string) => (await apiRequest('POST', '/api/reels/generate-text', { productInfo: text })).json(), onSuccess: (data: any) => { setGeneratedVariants(data.variants || []); toast({ title: "Textes générés", description: "Cliquez pour appliquer." }); }, onError: () => toast({ title: "Erreur IA", variant: "destructive" }), }); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) setImages(Array.from(e.target.files).slice(0, 4 - selectedLibraryImages.length)); }; const toggleLibraryImage = (media: Media) => { setSelectedLibraryImages(prev => { if (prev.find(m => m.id === media.id)) return prev.filter(m => m.id !== media.id); if (images.length + prev.length >= 4) { toast({ title: "4 images maximum.", variant: "destructive" }); return prev; } return [...prev, media]; }); }; const handleTtsPreview = async () => { const ttsText = overlayText.replace(/#\w+/g, '').replace(/[\uD800-\uDFFF\u2600-\u27BF]/g, '').replace(/\s+/g, ' ').trim(); if (!ttsText) return; try { const r = await apiRequest('POST', '/api/reels/tts-preview', { text: ttsText, ttsEngine, ttsVoice }); const data = await r.json(); if (data.success && data.audioBase64) new window.Audio(`data:audio/mp3;base64,${data.audioBase64}`).play(); } catch { toast({ title: "Erreur prévisualisation voix", variant: "destructive" }); } }; const togglePlayPreview = (track: AudioTrack) => { if (!audioRef.current) return; if (isPlaying === track.id) { audioRef.current.pause(); setIsPlaying(null); } else { audioRef.current.src = track.url; audioRef.current.play(); setIsPlaying(track.id); } }; const handleGenerate = async () => { if (totalSelected < 3) { toast({ title: "Minimum 3 images requises.", variant: "destructive" }); return; } setIsRendering(true); setVideoUrl(null); try { const formData = new FormData(); images.forEach(img => formData.append("images", img)); selectedLibraryImages.forEach(m => formData.append("existingImageUrls", m.originalUrl)); if (overlayText) formData.append("overlayText", overlayText); 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)); } const response = await fetch("/api/remotion/render", { method: "POST", body: formData, credentials: "include" }); if (!response.ok) { if (response.status === 401) handleUnauthorized("/api/remotion/render"); const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); } const data = await response.json(); if (data.jobId) { toast({ title: "Rendu démarré", description: "Veuillez patienter (~1-2 min)..." }); const checkStatus = async () => { try { const res = await fetch(`/api/remotion/render/status/${data.jobId}`, { credentials: "include" }); if (!res.ok) { if (res.status === 401) handleUnauthorized(`/api/remotion/render/status/${data.jobId}`); throw new Error("Erreur de suivi du rendu"); } const job = await res.json(); if (job.status === 'done') { setVideoUrl(job.url); if (overlayText && !publishDescription) setPublishDescription(overlayText); toast({ title: "Vidéo générée !", description: "Votre vidéo est prête à publier." }); setIsRendering(false); } else if (job.status === 'error') { toast({ title: "Erreur de rendu", description: job.error || "Échec du rendu", variant: "destructive" }); setIsRendering(false); } else { setTimeout(checkStatus, 3000); } } catch (e: any) { toast({ title: "Erreur de connexion", description: e.message, variant: "destructive" }); setIsRendering(false); } }; setTimeout(checkStatus, 3000); } else { // Fallback for older sync backend setVideoUrl(data.url); if (overlayText && !publishDescription) setPublishDescription(overlayText); toast({ title: "Vidéo générée !", description: "Votre vidéo est prête à publier." }); setIsRendering(false); } } catch (err: any) { toast({ title: "Erreur", description: err.message || "Impossible de créer la vidéo.", variant: "destructive" }); setIsRendering(false); } // Note: removed finally block so isRendering stays true during polling }; const handlePublish = async () => { if (!videoUrl) return; if (selectedPageIds.length === 0) { toast({ title: "Sélectionnez au moins une page.", variant: "destructive" }); return; } setIsPublishing(true); try { const response = await apiRequest('POST', '/api/remotion/publish', { videoUrl, pageIds: selectedPageIds, scheduledFor: scheduledDate?.toISOString(), description: publishDescription || undefined, }); const data = await response.json(); if (!data.success) throw new Error(data.results?.find((r: any) => r.error)?.error || "Erreur"); toast({ title: scheduledDate ? "Publication planifiée !" : "Vidéo publiée !", description: scheduledDate ? `Sera publiée le ${scheduledDate.toLocaleString('fr-FR')}` : "Publiée sur Facebook.", }); await queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'], refetchType: 'all' }); } catch (err: any) { toast({ title: "Erreur de publication", description: err.message, variant: "destructive" }); } finally { setIsPublishing(false); } }; return (
{sidebarOpen && (
setSidebarOpen(false)} /> )}
setSidebarOpen(false)} />
setSidebarOpen(!sidebarOpen)} />