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:
MichaelandClaude Sonnet 4.6 committed 2026-05-19 09:15:26 +02:00
1 parent 0209c3eacf
commit 797e12dde5
14 files changed
+152 -725

No files matched your search

+4 -31
View File
@@ -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) {
+2 -19
View File
@@ -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>