projects
This commit is contained in:
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user