mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-21 14:08:10 +02:00
feat(reels): voix Gemini complète, sous-titres mot à mot et rendu de qualité
Service ffmpeg-api réécrit (ffmpeg-service/app/) : - Appels FFmpeg asynchrones : le service ne se fige plus pendant un rendu - Vidéo récupérée par téléchargement (GET /files/…) au lieu de base64 en JSON - Vraies erreurs HTTP ; échec explicite si la voix demandée est impossible - 30 voix Gemini + ton de lecture (dynamique, chaleureux, promo, calme), clé en en-tête, modèle configurable avec repli - Edge TTS 7 (minutage des mots restauré), secours en voix françaises - Voix traitée : filtre, compression, niveau constant ; musique bouclée et baissée automatiquement sous la voix ; mix final à -14 LUFS - Sous-titres calés mot à mot (Whisper pour Gemini et la voix d'origine, à la place de ffsubsync), style Montserrat, placés hors des boutons Reels - Vidéo : plus de retouche luminosité forcée, scaling lanczos, HDR iPhone converti, BT.709, AAC 48 kHz 192k ; la vidéo s'allonge si la voix dépasse - Grand logo de fin affiché après la voix ; FFmpeg 7.0.2 épinglé, polices et modèle Whisper intégrés à l'image ; tests pytest et ruff Application : - Sélecteur de voix partagé (4 pages) : moteur, 30 voix, ton, écoute - Reel images : minutage réel des mots, interrupteur voix respecté - sync-info ne génère plus de voix à chaque frappe (estimation locale) - Stabilisation désactivée par défaut, route /reels/preview inutilisée retirée - Log « [ReelQueue] Worker démarré » pour vérifier la version déployée Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
This commit is contained in:
47 files changed
+2206
-2285
No files matched your search
@@ -16,6 +16,8 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { VoicePicker, type VoiceSettings } from "@/components/reels/voice-picker";
|
||||
import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices";
|
||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
||||
import type { SocialPage, Media } from "@shared/schema";
|
||||
import { SiFacebook, SiTiktok } from "react-icons/si";
|
||||
@@ -59,27 +61,17 @@ export default function MobileNewReel() {
|
||||
|
||||
// État TTS
|
||||
const [ttsEnabled, setTtsEnabled] = useState(false);
|
||||
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 [voiceSettings, setVoiceSettings] = useState<VoiceSettings>({
|
||||
engine: 'gemini',
|
||||
voice: DEFAULT_VOICE.gemini,
|
||||
style: DEFAULT_TTS_STYLE,
|
||||
});
|
||||
const { engine: ttsEngine, voice: ttsVoice, style: ttsStyle } = voiceSettings;
|
||||
|
||||
// État audio preview
|
||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||
const [stabilize, setStabilize] = useState(true); // Activé par défaut pour les Reels
|
||||
// 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);
|
||||
|
||||
// TTS Sync state
|
||||
@@ -271,6 +263,7 @@ export default function MobileNewReel() {
|
||||
ttsEnabled,
|
||||
ttsEngine,
|
||||
ttsVoice,
|
||||
ttsStyle,
|
||||
enableEndingEffect,
|
||||
});
|
||||
};
|
||||
@@ -492,82 +485,13 @@ export default function MobileNewReel() {
|
||||
<span>TTS — voix activée</span>
|
||||
</div>
|
||||
|
||||
{/* TTS Engine Selector */}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Label className="text-xs font-medium">Moteur:</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={ttsEngine === 'edge' ? 'default' : 'outline'}
|
||||
className="h-7 text-xs px-2"
|
||||
onClick={() => {
|
||||
setTtsEngine('edge');
|
||||
setTtsVoice('fr-FR-VivienneMultilingualNeural');
|
||||
}}
|
||||
>
|
||||
Edge
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={ttsEngine === 'gemini' ? 'default' : 'outline'}
|
||||
className="h-7 text-xs px-2"
|
||||
onClick={() => {
|
||||
setTtsEngine('gemini');
|
||||
setTtsVoice('fr-FR-Standard-B');
|
||||
}}
|
||||
>
|
||||
Gemini
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Selector */}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Label className="text-xs font-medium shrink-0">Voix:</Label>
|
||||
<Select value={ttsVoice} onValueChange={setTtsVoice}>
|
||||
<SelectTrigger className="h-7 text-xs flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ttsEngine === 'edge' ? (
|
||||
edgeVoices.map(v => (
|
||||
<SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>
|
||||
))
|
||||
) : (
|
||||
geminiVoices.map(v => (
|
||||
<SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="w-full h-8 text-xs"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const textToTest = overlayText || "Ceci est un test de voix.";
|
||||
try {
|
||||
const response = await apiRequest('POST', '/api/reels/tts-preview', {
|
||||
text: textToTest,
|
||||
ttsEngine,
|
||||
ttsVoice,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.audioBase64) {
|
||||
const audio = new Audio(`data:audio/mp3;base64,${data.audioBase64}`);
|
||||
audio.play();
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: "Erreur", description: "Impossible de lire la voix", variant: "destructive" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Play className="w-3 h-3 mr-1" /> Tester la voix
|
||||
</Button>
|
||||
<VoicePicker
|
||||
value={voiceSettings}
|
||||
onChange={setVoiceSettings}
|
||||
sampleText={overlayText}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
|
||||
{syncInfo && (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { VoicePicker, type VoiceSettings } from "@/components/reels/voice-picker";
|
||||
import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -33,23 +35,11 @@ export default function MobileRemotionVideoPage() {
|
||||
const [productInfo, setProductInfo] = useState("");
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
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 [voiceSettings, setVoiceSettings] = useState<VoiceSettings>({
|
||||
engine: 'gemini',
|
||||
voice: DEFAULT_VOICE.gemini,
|
||||
style: DEFAULT_TTS_STYLE,
|
||||
});
|
||||
const [musicFile, setMusicFile] = useState<File | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<AudioTrack | null>(null);
|
||||
const [musicVolume, setMusicVolume] = useState(0.3);
|
||||
@@ -143,16 +133,6 @@ export default function MobileRemotionVideoPage() {
|
||||
|
||||
const totalSelected = images.length + selectedLibraryImages.length;
|
||||
|
||||
const handleTtsPreview = async () => {
|
||||
const ttsText = stripForTTS(overlayText);
|
||||
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); }
|
||||
@@ -168,8 +148,10 @@ export default function MobileRemotionVideoPage() {
|
||||
images.forEach(img => formData.append("images", img));
|
||||
selectedLibraryImages.forEach(m => formData.append("existingImageUrls", m.originalUrl));
|
||||
if (overlayText) formData.append("overlayText", overlayText);
|
||||
formData.append("ttsEngine", ttsEngine);
|
||||
formData.append("ttsVoice", ttsVoice);
|
||||
formData.append("ttsEnabled", String(ttsEnabled));
|
||||
formData.append("ttsEngine", voiceSettings.engine);
|
||||
formData.append("ttsVoice", voiceSettings.voice);
|
||||
formData.append("ttsStyle", voiceSettings.style);
|
||||
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)); }
|
||||
@@ -354,41 +336,7 @@ export default function MobileRemotionVideoPage() {
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">TTS — voix activée</p>
|
||||
|
||||
{/* TTS Engine Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium">Moteur:</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant={ttsEngine === 'edge' ? 'default' : 'outline'} className="h-7 text-xs px-2"
|
||||
onClick={() => { setTtsEngine('edge'); setTtsVoice('fr-FR-VivienneMultilingualNeural'); }}>
|
||||
Edge
|
||||
</Button>
|
||||
<Button size="sm" variant={ttsEngine === 'gemini' ? 'default' : 'outline'} className="h-7 text-xs px-2"
|
||||
onClick={() => { setTtsEngine('gemini'); setTtsVoice('fr-FR-Standard-B'); }}>
|
||||
Gemini
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium shrink-0">Voix:</Label>
|
||||
<Select value={ttsVoice} onValueChange={setTtsVoice}>
|
||||
<SelectTrigger className="h-7 text-xs flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ttsEngine === 'edge' ? (
|
||||
edgeVoices.map(v => <SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>)
|
||||
) : (
|
||||
geminiVoices.map(v => <SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
||||
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
||||
</Button>
|
||||
<VoicePicker value={voiceSettings} onChange={setVoiceSettings} sampleText={overlayText} compact />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
+18
-101
@@ -25,6 +25,8 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { VoicePicker, type VoiceSettings } from "@/components/reels/voice-picker";
|
||||
import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices";
|
||||
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
|
||||
import type { SocialPage, Media } from "@shared/schema";
|
||||
import { SiFacebook, SiTiktok } from "react-icons/si";
|
||||
@@ -67,7 +69,8 @@ export default function NewReel() {
|
||||
const [musicVolume, setMusicVolume] = useState([25]);
|
||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||
const [drawText, setDrawText] = useState(true);
|
||||
const [stabilize, setStabilize] = useState(true); // default to true
|
||||
// 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);
|
||||
|
||||
// TTS Sync state
|
||||
@@ -79,24 +82,13 @@ export default function NewReel() {
|
||||
warnings: string[];
|
||||
} | null>(null);
|
||||
|
||||
// TTS Engine & Voice
|
||||
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' },
|
||||
];
|
||||
// Voix : moteur, voix et ton de lecture
|
||||
const [voiceSettings, setVoiceSettings] = useState<VoiceSettings>({
|
||||
engine: 'gemini',
|
||||
voice: DEFAULT_VOICE.gemini,
|
||||
style: DEFAULT_TTS_STYLE,
|
||||
});
|
||||
const { engine: ttsEngine, voice: ttsVoice, style: ttsStyle } = voiceSettings;
|
||||
|
||||
// Enable TTS by default on mobile
|
||||
useEffect(() => {
|
||||
@@ -413,6 +405,7 @@ export default function NewReel() {
|
||||
ttsEnabled,
|
||||
ttsEngine,
|
||||
ttsVoice,
|
||||
ttsStyle,
|
||||
drawText,
|
||||
stabilize: stabilize,
|
||||
enableEndingEffect,
|
||||
@@ -819,88 +812,12 @@ export default function NewReel() {
|
||||
<span>TTS — voix activée</span>
|
||||
</div>
|
||||
|
||||
{/* TTS Engine Selector */}
|
||||
<div className="flex items-center gap-4 mt-3">
|
||||
<Label className="text-sm font-medium">Moteur:</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={ttsEngine === 'edge' ? 'default' : 'outline'}
|
||||
onClick={() => {
|
||||
setTtsEngine('edge');
|
||||
setTtsVoice('fr-FR-VivienneMultilingualNeural');
|
||||
}}
|
||||
>
|
||||
Edge TTS
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={ttsEngine === 'gemini' ? 'default' : 'outline'}
|
||||
onClick={() => {
|
||||
setTtsEngine('gemini');
|
||||
setTtsVoice('fr-FR-Standard-B');
|
||||
}}
|
||||
>
|
||||
Gemini TTS
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Selector */}
|
||||
<div className="flex items-center gap-3 mt-3">
|
||||
<Label className="text-sm font-medium shrink-0">Voix:</Label>
|
||||
<Select
|
||||
value={ttsVoice}
|
||||
onValueChange={setTtsVoice}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ttsEngine === 'edge' ? (
|
||||
edgeVoices.map(v => (
|
||||
<SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>
|
||||
))
|
||||
) : (
|
||||
geminiVoices.map(v => (
|
||||
<SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const textToTest = overlayText || "Ceci est un test de voix pour votre vidéo.";
|
||||
try {
|
||||
const response = await apiRequest('POST', '/api/reels/tts-preview', {
|
||||
text: textToTest,
|
||||
ttsEngine,
|
||||
ttsVoice,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.audioBase64) {
|
||||
const audio = new Audio(`data:audio/mp3;base64,${data.audioBase64}`);
|
||||
audio.play();
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de tester la voix",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Play className="w-3 h-3 mr-2" />
|
||||
Tester la voix
|
||||
</Button>
|
||||
<div className="mt-3">
|
||||
<VoicePicker
|
||||
value={voiceSettings}
|
||||
onChange={setVoiceSettings}
|
||||
sampleText={overlayText}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{syncInfo && (
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useRef } from "react";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { VoicePicker, type VoiceSettings } from "@/components/reels/voice-picker";
|
||||
import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -34,23 +36,11 @@ export default function RemotionVideoPage() {
|
||||
const [productInfo, setProductInfo] = useState("");
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
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 [voiceSettings, setVoiceSettings] = useState<VoiceSettings>({
|
||||
engine: 'gemini',
|
||||
voice: DEFAULT_VOICE.gemini,
|
||||
style: DEFAULT_TTS_STYLE,
|
||||
});
|
||||
const [musicFile, setMusicFile] = useState<File | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<AudioTrack | null>(null);
|
||||
const [musicVolume, setMusicVolume] = useState(0.3);
|
||||
@@ -109,16 +99,6 @@ export default function RemotionVideoPage() {
|
||||
});
|
||||
};
|
||||
|
||||
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); }
|
||||
@@ -134,8 +114,10 @@ export default function RemotionVideoPage() {
|
||||
images.forEach(img => formData.append("images", img));
|
||||
selectedLibraryImages.forEach(m => formData.append("existingImageUrls", m.originalUrl));
|
||||
if (overlayText) formData.append("overlayText", overlayText);
|
||||
formData.append("ttsEngine", ttsEngine);
|
||||
formData.append("ttsVoice", ttsVoice);
|
||||
formData.append("ttsEnabled", String(ttsEnabled));
|
||||
formData.append("ttsEngine", voiceSettings.engine);
|
||||
formData.append("ttsVoice", voiceSettings.voice);
|
||||
formData.append("ttsStyle", voiceSettings.style);
|
||||
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)); }
|
||||
@@ -294,41 +276,7 @@ export default function RemotionVideoPage() {
|
||||
<div className="space-y-3 p-3 bg-muted/30 rounded-lg border">
|
||||
<p className="text-sm text-muted-foreground">TTS — voix activée</p>
|
||||
|
||||
{/* TTS Engine Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium">Moteur:</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant={ttsEngine === 'edge' ? 'default' : 'outline'} className="h-7 text-xs"
|
||||
onClick={() => { setTtsEngine('edge'); setTtsVoice('fr-FR-VivienneMultilingualNeural'); }}>
|
||||
Edge
|
||||
</Button>
|
||||
<Button size="sm" variant={ttsEngine === 'gemini' ? 'default' : 'outline'} className="h-7 text-xs"
|
||||
onClick={() => { setTtsEngine('gemini'); setTtsVoice('fr-FR-Standard-B'); }}>
|
||||
Gemini
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium shrink-0">Voix:</Label>
|
||||
<Select value={ttsVoice} onValueChange={setTtsVoice}>
|
||||
<SelectTrigger className="h-7 text-xs flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ttsEngine === 'edge' ? (
|
||||
edgeVoices.map(v => <SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>)
|
||||
) : (
|
||||
geminiVoices.map(v => <SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
||||
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
||||
</Button>
|
||||
<VoicePicker value={voiceSettings} onChange={setVoiceSettings} sampleText={overlayText} />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
Reference in new issue
Block a user