This commit is contained in:
2026-02-25 06:59:29 +00:00
parent 88fff9efae
commit acd7713c86
37 changed files with 1581 additions and 69 deletions
@@ -5,7 +5,7 @@ import { RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useClient } from 'hooks/useClient';
type Provider = 'openai' | 'elevenlabs';
@@ -39,20 +39,23 @@ export const TTSSection = () => {
const [model, setModel] = useState('kokoro');
const [voice, setVoice] = useState('af_heart');
const [voices, setVoices] = useState<string[]>([]);
const [voiceGroups, setVoiceGroups] = useState<{ label: string; voices: string[] }[]>([]);
const [voicesLoading, setVoicesLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const fetchVoices = async (p: Provider, u: string, key: string) => {
const fetchVoices = async (p: Provider, u: string, key: string, m?: string) => {
setVoicesLoading(true);
try {
const res = await client.post<{ voices?: string[]; error?: string }>('/server-settings/tts/voices', {
const res = await client.post<{ voices?: string[]; groups?: { label: string; voices: string[] }[]; error?: string }>('/server-settings/tts/voices', {
provider: p,
url: u,
apiKey: key || undefined,
model: m || undefined,
});
if (res.voices) setVoices(res.voices);
else setVoices([]);
setVoiceGroups(res.groups ?? []);
} catch {
setVoices([]);
} finally {
@@ -67,7 +70,7 @@ export const TTSSection = () => {
setApiKey(config.apiKey ?? '');
setModel(config.model);
setVoice(config.voice);
fetchVoices(config.provider, config.url ?? '', config.apiKey ?? '');
fetchVoices(config.provider, config.url ?? '', config.apiKey ?? '', config.model);
}, [config]);
const handleProviderChange = (v: Provider) => {
@@ -189,7 +192,7 @@ export const TTSSection = () => {
<span className="text-duck-dark/70 dark:text-foreground/70">Voice</span>
<button
type="button"
onClick={() => fetchVoices(provider, url, apiKey)}
onClick={() => fetchVoices(provider, url, apiKey, model)}
disabled={voicesLoading}
className="p-0.5 rounded hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer transition-colors disabled:opacity-40"
title="Refresh voices"
@@ -203,9 +206,19 @@ export const TTSSection = () => {
<SelectValue placeholder="Select a voice" />
</SelectTrigger>
<SelectContent className="z-[600] max-h-[300px]">
{voices.map((v) => (
<SelectItem key={v} value={v}>{v}</SelectItem>
))}
{voiceGroups.length > 0
? voiceGroups.map((g) => (
<SelectGroup key={g.label}>
<SelectLabel className="text-xs font-semibold text-duck-dark/50 dark:text-foreground/50">{g.label}</SelectLabel>
{g.voices.map((v) => (
<SelectItem key={v} value={v}>{v.replace(/^[a-z]{2}_/, '').replace(/^\w/, (c) => c.toUpperCase())}</SelectItem>
))}
</SelectGroup>
))
: voices.map((v) => (
<SelectItem key={v} value={v}>{v}</SelectItem>
))
}
</SelectContent>
</Select>
) : (