mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-16 19:59:42 +02:00
refactor(tts): replace Minimax+Freesound with Piper TTS
Remove Minimax Speech API and Freesound integrations entirely. Add Piper TTS as the sole TTS provider via configurable HTTP URL. - Add piper_config DB table (url field, per-user) - Add GET/POST /api/piper/config routes - Python: replace generate_tts_minimax with generate_tts_piper (GET ?text=, WAV→MP3) - Simplify generate_tts_with_subs: piper_url param replaces tts_provider+minimax fields - Drop ttsVoice/ttsProvider from all UI, API, and background job params - Settings page: replace Minimax+Freesound cards with single Piper URL input - Remove freeSoundService init from server startup and CSP headers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0209c3eacf
commit
797e12dde5
14 files changed
+152
-725
No files matched your search
@@ -58,7 +58,6 @@ export default function MobileNewReel() {
|
||||
|
||||
// État TTS
|
||||
const [ttsEnabled, setTtsEnabled] = useState(false);
|
||||
const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural");
|
||||
|
||||
// État audio preview
|
||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||
@@ -83,14 +82,14 @@ export default function MobileNewReel() {
|
||||
const timer = setTimeout(() => {
|
||||
apiRequest('POST', '/api/reels/sync-info', {
|
||||
text: overlayText,
|
||||
voice: ttsVoice,
|
||||
voice: "fr-FR-VivienneMultilingualNeural",
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => setSyncInfo(data))
|
||||
.catch(() => setSyncInfo(null));
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
}, [overlayText, ttsVoice, ttsEnabled]);
|
||||
}, [overlayText, ttsEnabled]);
|
||||
|
||||
|
||||
const { data: pages = [] } = useQuery<SocialPage[]>({
|
||||
@@ -231,7 +230,6 @@ export default function MobileNewReel() {
|
||||
stabilize: stabilize,
|
||||
drawText: true,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
enableEndingEffect,
|
||||
});
|
||||
};
|
||||
@@ -448,33 +446,9 @@ export default function MobileNewReel() {
|
||||
|
||||
{ttsEnabled && (
|
||||
<div className="mt-4 space-y-2 ml-1 p-3 bg-muted/30 rounded-lg border border-border/50">
|
||||
<Label className="flex items-center gap-2 mb-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Mic className="w-4 h-4" />
|
||||
Voix du narrateur
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ id: "fr-FR-VivienneMultilingualNeural", label: "Vivienne", type: "Femme", style: "bg-pink-500/10 border-pink-500/50" },
|
||||
{ id: "fr-FR-RemyMultilingualNeural", label: "Rémy", type: "Homme", style: "bg-blue-500/10 border-blue-500/50" },
|
||||
{ id: "fr-FR-DeniseNeural", label: "Denise", type: "Femme", style: "bg-purple-500/10 border-purple-500/50" },
|
||||
{ id: "fr-FR-HenriNeural", label: "Henri", type: "Homme", style: "bg-slate-500/10 border-slate-500/50" },
|
||||
{ id: "fr-FR-EloiseNeural", label: "Éloïse", type: "Enfant", style: "bg-orange-500/10 border-orange-500/50" }
|
||||
].map((voice) => (
|
||||
<div
|
||||
key={voice.id}
|
||||
onClick={() => setTtsVoice(voice.id)}
|
||||
className={`cursor-pointer p-3 rounded-md border-2 transition-all hover:bg-accent ${ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')
|
||||
? `border-primary ${voice.style}`
|
||||
: 'border-transparent bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold flex items-center justify-between text-sm">
|
||||
{voice.label}
|
||||
{(ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')) && <Check className="w-3 h-3 text-primary" />}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">{voice.type}</div>
|
||||
</div>
|
||||
))}
|
||||
<span>Piper TTS — voix configurée sur le serveur</span>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
@@ -487,7 +461,6 @@ export default function MobileNewReel() {
|
||||
try {
|
||||
const response = await apiRequest('POST', '/api/reels/tts-preview', {
|
||||
text: textToTest,
|
||||
voice: ttsVoice
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.audioBase64) {
|
||||
|
||||
@@ -18,13 +18,6 @@ import type { Media, SocialPage } from "@shared/schema";
|
||||
|
||||
interface AudioTrack { id: string; title: string; fileName: string; url: string; duration: number; }
|
||||
|
||||
const TTS_VOICES = [
|
||||
{ id: "fr-FR-VivienneMultilingualNeural", label: "Vivienne", type: "Femme" },
|
||||
{ id: "fr-FR-RemyMultilingualNeural", label: "Rémy", type: "Homme" },
|
||||
{ id: "fr-FR-DeniseNeural", label: "Denise", type: "Femme" },
|
||||
{ id: "fr-FR-HenriNeural", label: "Henri", type: "Homme" },
|
||||
{ id: "fr-FR-EloiseNeural", label: "Éloïse", type: "Enfant" },
|
||||
];
|
||||
|
||||
function stripForTTS(text: string) {
|
||||
return text.replace(/#\w+/g, '').replace(/[\uD800-\uDFFF]/g, '').replace(/[\u2600-\u27BF]/g, '').replace(/\s+/g, ' ').trim();
|
||||
@@ -38,7 +31,6 @@ export default function MobileRemotionVideoPage() {
|
||||
const [productInfo, setProductInfo] = useState("");
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||
const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural");
|
||||
const [musicFile, setMusicFile] = useState<File | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<AudioTrack | null>(null);
|
||||
const [musicVolume, setMusicVolume] = useState(0.3);
|
||||
@@ -119,7 +111,7 @@ export default function MobileRemotionVideoPage() {
|
||||
const ttsText = stripForTTS(overlayText);
|
||||
if (!ttsText) return;
|
||||
try {
|
||||
const r = await apiRequest('POST', '/api/reels/tts-preview', { text: ttsText, voice: ttsVoice });
|
||||
const r = await apiRequest('POST', '/api/reels/tts-preview', { text: ttsText });
|
||||
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" }); }
|
||||
@@ -140,7 +132,6 @@ 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);
|
||||
if (ttsEnabled && overlayText) formData.append("ttsVoice", ttsVoice);
|
||||
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)); }
|
||||
@@ -320,15 +311,7 @@ export default function MobileRemotionVideoPage() {
|
||||
</div>
|
||||
{ttsEnabled && (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{TTS_VOICES.map(v => (
|
||||
<div key={v.id} onClick={() => setTtsVoice(v.id)}
|
||||
className={`cursor-pointer p-1.5 rounded border-2 text-center ${ttsVoice === v.id ? 'border-primary bg-primary/10' : 'border-transparent bg-muted/30'}`}>
|
||||
<div className="font-semibold text-xs">{v.label}</div>
|
||||
<div className="text-[10px] text-muted-foreground">{v.type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Piper TTS — voix configurée sur le serveur</p>
|
||||
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
||||
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
||||
</Button>
|
||||
|
||||
@@ -65,8 +65,6 @@ export default function NewReel() {
|
||||
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
||||
const [musicVolume, setMusicVolume] = useState([25]);
|
||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||
const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural");
|
||||
const [ttsProvider, setTtsProvider] = useState<"edge_tts" | "minimax">("edge_tts");
|
||||
const [drawText, setDrawText] = useState(true);
|
||||
const [stabilize, setStabilize] = useState(true); // default to true
|
||||
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
|
||||
@@ -88,23 +86,23 @@ export default function NewReel() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto-calculate TTS sync when text or voice changes (Edge TTS only)
|
||||
// Auto-calculate TTS sync when text changes (edge_tts fallback)
|
||||
useEffect(() => {
|
||||
if (!ttsEnabled || !overlayText.trim() || ttsProvider !== "edge_tts") {
|
||||
if (!ttsEnabled || !overlayText.trim()) {
|
||||
setSyncInfo(null);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
apiRequest('POST', '/api/reels/sync-info', {
|
||||
text: overlayText,
|
||||
voice: ttsVoice,
|
||||
voice: "fr-FR-VivienneMultilingualNeural",
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => setSyncInfo(data))
|
||||
.catch(() => setSyncInfo(null));
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
}, [overlayText, ttsVoice, ttsEnabled, ttsProvider]);
|
||||
}, [overlayText, ttsEnabled]);
|
||||
|
||||
|
||||
// État audio preview
|
||||
@@ -362,8 +360,6 @@ export default function NewReel() {
|
||||
scheduledFor: scheduledDate?.toISOString(),
|
||||
musicVolume: musicVolume[0] / 100,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
drawText,
|
||||
stabilize: stabilize,
|
||||
enableEndingEffect,
|
||||
@@ -764,60 +760,9 @@ export default function NewReel() {
|
||||
|
||||
{ttsEnabled && (
|
||||
<div className="mt-4 space-y-2 ml-12 p-4 bg-muted/30 rounded-lg border border-border/50">
|
||||
<Label className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Mic className="w-4 h-4" />
|
||||
Moteur TTS
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setTtsProvider("edge_tts"); setTtsVoice("fr-FR-VivienneMultilingualNeural"); }}
|
||||
className={`p-2 rounded-md border-2 text-sm font-medium transition-all ${ttsProvider === "edge_tts" ? "border-primary bg-primary/10" : "border-transparent bg-muted/30 hover:bg-accent"}`}
|
||||
>
|
||||
Edge TTS (local)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setTtsProvider("minimax"); setTtsVoice("French_Female_News"); }}
|
||||
className={`p-2 rounded-md border-2 text-sm font-medium transition-all ${ttsProvider === "minimax" ? "border-primary bg-primary/10" : "border-transparent bg-muted/30 hover:bg-accent"}`}
|
||||
>
|
||||
Minimax (premium)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Label className="flex items-center gap-2 mt-3">
|
||||
Voix du narrateur
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(ttsProvider === "edge_tts" ? [
|
||||
{ id: "fr-FR-VivienneMultilingualNeural", label: "Vivienne", type: "Femme (Défaut)", style: "bg-pink-500/10 border-pink-500/50" },
|
||||
{ id: "fr-FR-RemyMultilingualNeural", label: "Rémy", type: "Homme", style: "bg-blue-500/10 border-blue-500/50" },
|
||||
{ id: "fr-FR-DeniseNeural", label: "Denise", type: "Femme (Calme)", style: "bg-purple-500/10 border-purple-500/50" },
|
||||
{ id: "fr-FR-HenriNeural", label: "Henri", type: "Homme (Grave)", style: "bg-slate-500/10 border-slate-500/50" },
|
||||
{ id: "fr-FR-EloiseNeural", label: "Éloïse", type: "Enfant", style: "bg-orange-500/10 border-orange-500/50" }
|
||||
] : [
|
||||
{ id: "French_Female_News", label: "News Féminin", type: "Femme (Défaut)", style: "bg-pink-500/10 border-pink-500/50" },
|
||||
{ id: "French_Male_Speech_New", label: "Speech Masculin", type: "Homme", style: "bg-blue-500/10 border-blue-500/50" },
|
||||
{ id: "French_MovieLeadFemale", label: "Cinéma Femme", type: "Femme (Dramatique)", style: "bg-purple-500/10 border-purple-500/50" },
|
||||
{ id: "French_CasualMan", label: "Casual Homme", type: "Homme (Décontracté)", style: "bg-slate-500/10 border-slate-500/50" },
|
||||
{ id: "French_FemaleAnchor", label: "Présentatrice", type: "Femme (Média)", style: "bg-rose-500/10 border-rose-500/50" },
|
||||
{ id: "French_MaleNarrator", label: "Narrateur", type: "Homme (Narration)", style: "bg-indigo-500/10 border-indigo-500/50" }
|
||||
]).map((voice) => (
|
||||
<div
|
||||
key={voice.id}
|
||||
onClick={() => setTtsVoice(voice.id)}
|
||||
className={`cursor-pointer p-3 rounded-md border-2 transition-all hover:bg-accent ${ttsVoice === voice.id
|
||||
? `border-primary ${voice.style}`
|
||||
: 'border-transparent bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold flex items-center justify-between">
|
||||
{voice.label}
|
||||
{ttsVoice === voice.id && <Check className="w-3 h-3 text-primary" />}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{voice.type}</div>
|
||||
</div>
|
||||
))}
|
||||
<span>Piper TTS — voix configurée sur le serveur</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex gap-2">
|
||||
@@ -831,8 +776,6 @@ export default function NewReel() {
|
||||
try {
|
||||
const response = await apiRequest('POST', '/api/reels/tts-preview', {
|
||||
text: textToTest,
|
||||
voice: ttsVoice,
|
||||
ttsProvider,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.audioBase64) {
|
||||
@@ -853,15 +796,7 @@ export default function NewReel() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ttsProvider === "minimax" && (
|
||||
<div className="p-3 rounded-lg border mt-3 bg-blue-500/10 border-blue-500/30">
|
||||
<p className="text-xs text-blue-600 dark:text-blue-400">
|
||||
Minimax utilise ffsubsync pour la synchronisation des sous-titres. Configurez votre clé API dans Paramètres.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ttsProvider === "edge_tts" && syncInfo && (
|
||||
{syncInfo && (
|
||||
<div className={`p-3 rounded-lg border mt-3 ${syncInfo.isHealthy ? 'bg-green-500/10 border-green-500/30' : 'bg-yellow-500/10 border-yellow-500/30'}`}>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">Sync texte/voix</span>
|
||||
|
||||
@@ -15,13 +15,6 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { DateTimePicker } from "@/components/datetime-picker";
|
||||
import type { Media, SocialPage } from "@shared/schema";
|
||||
|
||||
const TTS_VOICES = [
|
||||
{ id: "fr-FR-VivienneMultilingualNeural", label: "Vivienne", type: "Femme" },
|
||||
{ id: "fr-FR-RemyMultilingualNeural", label: "Rémy", type: "Homme" },
|
||||
{ id: "fr-FR-DeniseNeural", label: "Denise", type: "Femme" },
|
||||
{ id: "fr-FR-HenriNeural", label: "Henri", type: "Homme" },
|
||||
{ id: "fr-FR-EloiseNeural", label: "Éloïse", type: "Enfant" },
|
||||
];
|
||||
|
||||
interface AudioTrack {
|
||||
id: string;
|
||||
@@ -39,7 +32,6 @@ export default function RemotionVideoPage() {
|
||||
const [productInfo, setProductInfo] = useState("");
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||
const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural");
|
||||
const [musicFile, setMusicFile] = useState<File | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<AudioTrack | null>(null);
|
||||
const [musicVolume, setMusicVolume] = useState(0.3);
|
||||
@@ -86,7 +78,7 @@ export default function RemotionVideoPage() {
|
||||
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, voice: ttsVoice });
|
||||
const r = await apiRequest('POST', '/api/reels/tts-preview', { text: ttsText });
|
||||
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" }); }
|
||||
@@ -107,7 +99,6 @@ 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);
|
||||
if (ttsEnabled && overlayText) formData.append("ttsVoice", ttsVoice);
|
||||
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)); }
|
||||
@@ -261,15 +252,7 @@ export default function RemotionVideoPage() {
|
||||
</div>
|
||||
{ttsEnabled && (
|
||||
<div className="space-y-3 p-3 bg-muted/30 rounded-lg border">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{TTS_VOICES.map(v => (
|
||||
<div key={v.id} onClick={() => setTtsVoice(v.id)}
|
||||
className={`cursor-pointer p-2 rounded border-2 text-center transition-all ${ttsVoice === v.id ? 'border-primary bg-primary/10' : 'border-transparent bg-muted/30'}`}>
|
||||
<div className="font-semibold text-sm">{v.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{v.type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Piper TTS — voix configurée sur le serveur</p>
|
||||
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
||||
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
||||
</Button>
|
||||
|
||||
+29
-161
@@ -30,8 +30,7 @@ export default function Settings() {
|
||||
const [ffmpegApiUrl, setFfmpegApiUrl] = useState("");
|
||||
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
||||
const [externalApiKey, setExternalApiKey] = useState("");
|
||||
const [minimaxApiKey, setMinimaxApiKey] = useState("");
|
||||
const [minimaxGroupId, setMinimaxGroupId] = useState("");
|
||||
const [piperUrl, setPiperUrl] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||
@@ -62,8 +61,8 @@ export default function Settings() {
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const { data: minimaxConfig } = useQuery({
|
||||
queryKey: ['/api/minimax/config'],
|
||||
const { data: piperConfig } = useQuery({
|
||||
queryKey: ['/api/piper/config'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,50 +78,11 @@ export default function Settings() {
|
||||
}
|
||||
}, [cloudinaryConfig]);
|
||||
|
||||
const { data: freesoundConfig } = useQuery({
|
||||
queryKey: ['/api/freesound/config'],
|
||||
});
|
||||
|
||||
const [freesoundClientId, setFreesoundClientId] = useState("");
|
||||
const [freesoundClientSecret, setFreesoundClientSecret] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (freesoundConfig) {
|
||||
setFreesoundClientId((freesoundConfig as any).clientId || "");
|
||||
setFreesoundClientSecret((freesoundConfig as any).clientSecret || ""); // Usually masked
|
||||
if (piperConfig) {
|
||||
setPiperUrl((piperConfig as any).url || "");
|
||||
}
|
||||
}, [freesoundConfig]);
|
||||
|
||||
const hasExistingFreesoundConfig = !!freesoundConfig;
|
||||
|
||||
const saveFreesoundMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const payload: any = {
|
||||
clientId: freesoundClientId,
|
||||
};
|
||||
|
||||
if (freesoundClientSecret && freesoundClientSecret.trim() !== "" && freesoundClientSecret !== "********") {
|
||||
payload.clientSecret = freesoundClientSecret;
|
||||
}
|
||||
|
||||
return apiRequest('POST', '/api/freesound/config', payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/freesound/config'] });
|
||||
toast({
|
||||
title: "Configuration sauvegardée",
|
||||
description: "Vos paramètres Freesound ont été enregistrés",
|
||||
});
|
||||
// On garde l'affichage tel quel ou on remet masked si retourné
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de sauvegarder la configuration Freesound",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [piperConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openrouterConfig) {
|
||||
@@ -264,33 +224,23 @@ export default function Settings() {
|
||||
});
|
||||
|
||||
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
||||
const hasExistingMinimaxConfig = !!(minimaxConfig as any)?.apiKey;
|
||||
const hasExistingMinimaxGroupId = !!(minimaxConfig as any)?.groupId;
|
||||
const hasExistingPiperConfig = !!(piperConfig as any)?.url;
|
||||
|
||||
const saveMinimaxMutation = useMutation({
|
||||
const savePiperMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const payload: any = {};
|
||||
if (minimaxApiKey && minimaxApiKey.trim() !== "") {
|
||||
payload.apiKey = minimaxApiKey;
|
||||
}
|
||||
if (minimaxGroupId && minimaxGroupId.trim() !== "") {
|
||||
payload.groupId = minimaxGroupId;
|
||||
}
|
||||
return apiRequest('POST', '/api/minimax/config', payload);
|
||||
return apiRequest('POST', '/api/piper/config', { url: piperUrl });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/minimax/config'] });
|
||||
setMinimaxApiKey("");
|
||||
setMinimaxGroupId("");
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/piper/config'] });
|
||||
toast({
|
||||
title: "Configuration sauvegardée",
|
||||
description: "Votre clé API Minimax a été enregistrée",
|
||||
description: "L'URL du serveur Piper TTS a été enregistrée",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de sauvegarder la configuration Minimax",
|
||||
description: "Impossible de sauvegarder la configuration Piper",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
@@ -546,53 +496,39 @@ export default function Settings() {
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader className="p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SettingsIcon className="w-5 h-5" />
|
||||
Configuration Freesound (Musique)
|
||||
<Mic className="w-5 h-5" />
|
||||
Configuration Piper TTS (Voix)
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configurez l'API Freesound pour la recherche de musique dans les Reels
|
||||
Configurez l'URL de votre serveur Piper TTS pour la synthèse vocale des Reels
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="freesoundClientId">Client ID</Label>
|
||||
<Label htmlFor="piperUrl">URL du serveur Piper</Label>
|
||||
<Input
|
||||
id="freesoundClientId"
|
||||
type="text"
|
||||
value={freesoundClientId}
|
||||
onChange={(e) => setFreesoundClientId(e.target.value)}
|
||||
placeholder="Votre Client ID Freesound"
|
||||
data-testid="input-freesound-client-id"
|
||||
id="piperUrl"
|
||||
type="url"
|
||||
value={piperUrl}
|
||||
onChange={(e) => setPiperUrl(e.target.value)}
|
||||
placeholder="http://piper-server:5000/api/tts"
|
||||
data-testid="input-piper-url"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Disponible dans vos paramètres d'API Freesound
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="freesoundClientSecret">Client Secret</Label>
|
||||
<Input
|
||||
id="freesoundClientSecret"
|
||||
type="password"
|
||||
value={freesoundClientSecret}
|
||||
onChange={(e) => setFreesoundClientSecret(e.target.value)}
|
||||
placeholder={hasExistingFreesoundConfig ? "••••••••" : "Votre Client Secret"}
|
||||
data-testid="input-freesound-client-secret"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{hasExistingFreesoundConfig ? (
|
||||
<span className="text-green-600 dark:text-green-400">✓ Configuré (Laissez vide pour conserver)</span>
|
||||
{hasExistingPiperConfig ? (
|
||||
<span className="text-green-600 dark:text-green-400">✓ Configuré</span>
|
||||
) : (
|
||||
"Votre clé secrète ne sera jamais exposée"
|
||||
"URL de l'endpoint HTTP GET de votre serveur Piper (ex: http://piper:5000/api/tts)"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveFreesoundMutation.mutate()}
|
||||
disabled={saveFreesoundMutation.isPending || !freesoundClientId}
|
||||
data-testid="button-save-freesound"
|
||||
onClick={() => savePiperMutation.mutate()}
|
||||
disabled={savePiperMutation.isPending || !piperUrl.trim()}
|
||||
data-testid="button-save-piper"
|
||||
className="w-full"
|
||||
>
|
||||
{saveFreesoundMutation.isPending ? "Enregistrement..." : "Enregistrer Freesound"}
|
||||
{savePiperMutation.isPending ? "Enregistrement..." : "Enregistrer Piper TTS"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -657,74 +593,6 @@ export default function Settings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader className="p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mic className="w-5 h-5" />
|
||||
Configuration Minimax Speech (TTS)
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configurez l'API Minimax pour générer des voix françaises premium dans les Reels
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="minimaxApiKey">Clé API Minimax</Label>
|
||||
<Input
|
||||
id="minimaxApiKey"
|
||||
type="password"
|
||||
value={minimaxApiKey}
|
||||
onChange={(e) => setMinimaxApiKey(e.target.value)}
|
||||
placeholder={hasExistingMinimaxConfig ? "••••••••••••••••" : "eyJhbGci..."}
|
||||
data-testid="input-minimax-api-key"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{hasExistingMinimaxConfig ? (
|
||||
<span className="text-green-600 dark:text-green-400">✓ Clé API configurée - Laissez vide pour conserver</span>
|
||||
) : (
|
||||
"Votre clé API depuis platform.minimaxi.com"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="minimaxGroupId">Group ID Minimax</Label>
|
||||
<Input
|
||||
id="minimaxGroupId"
|
||||
type="text"
|
||||
value={minimaxGroupId}
|
||||
onChange={(e) => setMinimaxGroupId(e.target.value)}
|
||||
placeholder={hasExistingMinimaxGroupId ? "••••••••••••••••" : "1234567890"}
|
||||
data-testid="input-minimax-group-id"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{hasExistingMinimaxGroupId ? (
|
||||
<span className="text-green-600 dark:text-green-400">✓ Group ID configuré - Laissez vide pour conserver</span>
|
||||
) : (
|
||||
"Requis pour les quotas corrects — trouvez-le sur platform.minimaxi.com dans votre profil"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/50 p-3 space-y-1">
|
||||
<p className="text-xs font-medium">Voix françaises disponibles :</p>
|
||||
<div className="grid grid-cols-2 gap-1 text-xs text-muted-foreground">
|
||||
<span>• French_Female_News</span>
|
||||
<span>• French_Male_Speech_New</span>
|
||||
<span>• French_MovieLeadFemale</span>
|
||||
<span>• French_CasualMan</span>
|
||||
<span>• French_FemaleAnchor</span>
|
||||
<span>• French_MaleNarrator</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveMinimaxMutation.mutate()}
|
||||
disabled={saveMinimaxMutation.isPending || (!minimaxApiKey.trim() && !minimaxGroupId.trim() && !hasExistingMinimaxConfig)}
|
||||
data-testid="button-save-minimax"
|
||||
className="w-full"
|
||||
>
|
||||
{saveMinimaxMutation.isPending ? "Enregistrement..." : "Enregistrer Minimax"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{isAdmin && (
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
|
||||
Reference in new issue
Block a user