mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-20 13:38:10 +02:00
Qwen TTS : serveur GPU distant (Unraid) réglé dans les paramètres
- qwen-tts : GPU NVIDIA détecté automatiquement (bfloat16), image CUDA 12.8 (RTX 50xx comprises) via TORCH_VARIANT=cu128, docker-compose.gpu.yml dédié (clé obligatoire, choix de la carte) et guide d'installation Unraid. - Paramètres → « Qwen TTS (voix locale) » : adresse + clé du service, test de connexion (modèle et carte graphique affichés), stockés en base (app_config.qwen_tts_url / qwen_tts_api_key) et transmis à ffmpeg-api à chaque voix ; QWEN_TTS_URL reste un défaut. - Le service qwen-tts CPU du docker-compose principal devient optionnel (profil qwen-local). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
This commit is contained in:
24 files changed
+494
-51
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Cpu, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, getErrorMessage, queryClient } from "@/lib/queryClient";
|
||||
|
||||
interface QwenConfig {
|
||||
url: string;
|
||||
hasApiKey: boolean;
|
||||
}
|
||||
|
||||
interface QwenTestResult {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
model?: string;
|
||||
device?: string;
|
||||
voices?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Réglages du service Qwen3-TTS : voix locale gratuite, idéalement sur un
|
||||
* serveur avec carte graphique (ex. Unraid). C'est le service FFmpeg qui
|
||||
* l'appelle : le test vérifie donc la connexion depuis celui-ci.
|
||||
*/
|
||||
export function QwenSettingsCard() {
|
||||
const { toast } = useToast();
|
||||
const [url, setUrl] = useState("");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [test, setTest] = useState<QwenTestResult | null>(null);
|
||||
|
||||
const { data: config } = useQuery<QwenConfig>({ queryKey: ["/api/settings/qwen"] });
|
||||
|
||||
useEffect(() => {
|
||||
if (config) setUrl(config.url);
|
||||
}, [config]);
|
||||
|
||||
const refresh = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/settings/qwen"] });
|
||||
// Le choix du moteur « Qwen » dépend de la disponibilité du service
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/reels/voices"] });
|
||||
};
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () => apiRequest("POST", "/api/settings/qwen", { url, apiKey }),
|
||||
onSuccess: () => {
|
||||
refresh();
|
||||
setApiKey("");
|
||||
toast({ title: "Configuration sauvegardée", description: "Service Qwen TTS enregistré" });
|
||||
},
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: getErrorMessage(error, "Impossible de sauvegarder la configuration Qwen"),
|
||||
variant: "destructive",
|
||||
}),
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: () => apiRequest("DELETE", "/api/settings/qwen"),
|
||||
onSuccess: () => {
|
||||
refresh();
|
||||
setUrl("");
|
||||
setApiKey("");
|
||||
setTest(null);
|
||||
toast({ title: "Configuration supprimée", description: "Service Qwen TTS retiré" });
|
||||
},
|
||||
});
|
||||
|
||||
const runTest = useMutation({
|
||||
mutationFn: async () => {
|
||||
const response = await apiRequest("POST", "/api/settings/qwen/test", { url, apiKey });
|
||||
return (await response.json()) as QwenTestResult;
|
||||
},
|
||||
onSuccess: setTest,
|
||||
onError: (error) => setTest({ ok: false, error: getErrorMessage(error, "Test impossible") }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader className="p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5" />
|
||||
Qwen TTS (voix locale)
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Voix française gratuite et expressive, générée par votre propre serveur (idéalement avec carte
|
||||
graphique NVIDIA, ex. Unraid). Voir qwen-tts/docker-compose.gpu.yml.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="qwenTtsUrl">Adresse du service</Label>
|
||||
<Input
|
||||
id="qwenTtsUrl"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="http://192.168.1.20:8001"
|
||||
data-testid="input-qwen-url"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="qwenTtsApiKey">Clé d'accès (API_KEY du service)</Label>
|
||||
<Input
|
||||
id="qwenTtsApiKey"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={config?.hasApiKey ? "••••••••••••••••" : "Clé définie sur le serveur Qwen"}
|
||||
data-testid="input-qwen-api-key"
|
||||
/>
|
||||
{config?.hasApiKey && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">✓ Clé enregistrée (laisser vide pour la garder)</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{test && (
|
||||
<p className={`text-sm ${test.ok ? "text-green-600 dark:text-green-400" : "text-destructive"}`}>
|
||||
{test.ok
|
||||
? `✓ Connecté : ${test.device ?? "?"} — ${test.model ?? ""} (${test.voices ?? 0} voix)`
|
||||
: `✗ ${test.error}`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => runTest.mutate()}
|
||||
disabled={runTest.isPending || (!url && !config?.url)}
|
||||
data-testid="button-test-qwen"
|
||||
>
|
||||
{runTest.isPending && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||
Tester la connexion
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => save.mutate()}
|
||||
disabled={save.isPending || !url}
|
||||
data-testid="button-save-qwen"
|
||||
>
|
||||
{save.isPending ? "Enregistrement..." : "Enregistrer Qwen TTS"}
|
||||
</Button>
|
||||
{config?.url && (
|
||||
<Button variant="outline" onClick={() => remove.mutate()} disabled={remove.isPending}>
|
||||
Retirer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export function VoicePicker({ value, onChange, sampleText, compact = false, onPr
|
||||
className={compact ? "h-7 text-xs" : undefined}
|
||||
variant={value.engine === "qwen" ? "default" : "outline"}
|
||||
disabled={!qwenAvailable && value.engine !== "qwen"}
|
||||
title={qwenAvailable ? "Voix locale, gratuite, plus lente à générer" : "Service Qwen TTS non démarré"}
|
||||
title={qwenAvailable ? "Voix locale, gratuite, plus lente à générer" : "Service Qwen TTS non configuré ou injoignable (Paramètres → Qwen TTS)"}
|
||||
onClick={() => setEngine("qwen")}
|
||||
>
|
||||
Qwen (locale)
|
||||
|
||||
Reference in new issue
Block a user