Use openai compatible as vendor

This commit is contained in:
Xin Wang
2026-02-12 18:44:55 +08:00
parent 260ff621bf
commit ff3a03b1ad
23 changed files with 822 additions and 905 deletions

View File

@@ -1,12 +1,12 @@
import React, { useEffect, useState, useRef } from 'react';
import { Search, Mic2, Play, Pause, Upload, Filter, Plus, Volume2, Sparkles, ChevronDown, Pencil, Trash2 } from 'lucide-react';
import { Search, Mic2, Play, Pause, Upload, Filter, Plus, Volume2, Pencil, Trash2 } from 'lucide-react';
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI';
import { Voice } from '../types';
import { createVoice, deleteVoice, fetchVoices, previewVoice, updateVoice } from '../services/backendApi';
const SILICONFLOW_DEFAULT_MODEL = 'FunAudioLLM/CosyVoice2-0.5B';
const OPENAI_COMPATIBLE_DEFAULT_MODEL = 'FunAudioLLM/CosyVoice2-0.5B';
const buildSiliconflowVoiceKey = (rawId: string, model: string): string => {
const buildOpenAICompatibleVoiceKey = (rawId: string, model: string): string => {
const id = (rawId || '').trim();
if (!id) return `${model}:anna`;
return id.includes(':') ? id : `${model}:${id}`;
@@ -15,7 +15,7 @@ const buildSiliconflowVoiceKey = (rawId: string, model: string): string => {
export const VoiceLibraryPage: React.FC = () => {
const [voices, setVoices] = useState<Voice[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [vendorFilter, setVendorFilter] = useState<'all' | 'Ali' | 'Volcano' | 'Minimax' | '硅基流动' | 'SiliconFlow'>('all');
const [vendorFilter, setVendorFilter] = useState<'OpenAI Compatible'>('OpenAI Compatible');
const [genderFilter, setGenderFilter] = useState<'all' | 'Male' | 'Female'>('all');
const [langFilter, setLangFilter] = useState<'all' | 'zh' | 'en'>('all');
@@ -44,7 +44,7 @@ export const VoiceLibraryPage: React.FC = () => {
const filteredVoices = voices.filter((voice) => {
const matchesSearch = voice.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesVendor = vendorFilter === 'all' || voice.vendor === vendorFilter;
const matchesVendor = voice.vendor === vendorFilter;
const matchesGender = genderFilter === 'all' || voice.gender === genderFilter;
const matchesLang = langFilter === 'all' || voice.language === langFilter;
return matchesSearch && matchesVendor && matchesGender && matchesLang;
@@ -138,12 +138,7 @@ export const VoiceLibraryPage: React.FC = () => {
value={vendorFilter}
onChange={(e) => setVendorFilter(e.target.value as any)}
>
<option value="all"></option>
<option value="硅基流动"> (SiliconFlow)</option>
<option value="SiliconFlow">SiliconFlow</option>
<option value="Ali"> (Ali)</option>
<option value="Volcano"> (Volcano)</option>
<option value="Minimax">Minimax</option>
<option value="OpenAI Compatible">OpenAI Compatible</option>
</select>
</div>
<div className="flex items-center space-x-2">
@@ -187,15 +182,12 @@ export const VoiceLibraryPage: React.FC = () => {
<TableRow key={voice.id}>
<TableCell className="font-medium">
<div className="flex flex-col">
<span className="flex items-center text-white">
{voice.vendor === '硅基流动' && <Sparkles className="w-3 h-3 text-primary mr-1.5" />}
{voice.name}
</span>
<span className="flex items-center text-white">{voice.name}</span>
{voice.description && <span className="text-xs text-muted-foreground">{voice.description}</span>}
</div>
</TableCell>
<TableCell>
<Badge variant={voice.vendor === '硅基流动' ? 'default' : 'outline'}>{voice.vendor}</Badge>
<Badge variant="outline">{voice.vendor}</Badge>
</TableCell>
<TableCell className="text-muted-foreground">{voice.gender === 'Male' ? '男' : '女'}</TableCell>
<TableCell className="text-muted-foreground">{voice.language === 'zh' ? '中文' : 'English'}</TableCell>
@@ -254,17 +246,15 @@ const AddVoiceModal: React.FC<{
onSuccess: (voice: Voice) => Promise<void>;
initialVoice?: Voice;
}> = ({ isOpen, onClose, onSuccess, initialVoice }) => {
const [vendor, setVendor] = useState<'硅基流动' | 'Ali' | 'Volcano' | 'Minimax'>('硅基流动');
const [vendor, setVendor] = useState<'OpenAI Compatible'>('OpenAI Compatible');
const [name, setName] = useState('');
const [sfModel, setSfModel] = useState(SILICONFLOW_DEFAULT_MODEL);
const [openaiCompatibleModel, setOpenaiCompatibleModel] = useState(OPENAI_COMPATIBLE_DEFAULT_MODEL);
const [sfVoiceId, setSfVoiceId] = useState('FunAudioLLM/CosyVoice2-0.5B:anna');
const [sfSpeed, setSfSpeed] = useState(1);
const [sfGain, setSfGain] = useState(0);
const [sfPitch, setSfPitch] = useState(0);
const [model, setModel] = useState('');
const [voiceKey, setVoiceKey] = useState('');
const [gender, setGender] = useState('Female');
const [language, setLanguage] = useState('zh');
const [description, setDescription] = useState('');
@@ -278,17 +268,15 @@ const AddVoiceModal: React.FC<{
useEffect(() => {
if (!initialVoice) return;
const nextVendor = initialVoice.vendor === 'SiliconFlow' ? '硅基流动' : initialVoice.vendor;
const nextModel = initialVoice.model || SILICONFLOW_DEFAULT_MODEL;
const defaultVoiceKey = buildSiliconflowVoiceKey(initialVoice.id || initialVoice.name || '', nextModel);
setVendor((nextVendor as any) || '硅基流动');
const nextVendor = 'OpenAI Compatible';
const nextModel = initialVoice.model || OPENAI_COMPATIBLE_DEFAULT_MODEL;
const defaultVoiceKey = buildOpenAICompatibleVoiceKey(initialVoice.id || initialVoice.name || '', nextModel);
setVendor(nextVendor);
setName(initialVoice.name || '');
setGender(initialVoice.gender || 'Female');
setLanguage(initialVoice.language || 'zh');
setDescription(initialVoice.description || '');
setModel(initialVoice.model || '');
setVoiceKey(initialVoice.voiceKey || '');
setSfModel(nextModel);
setOpenaiCompatibleModel(nextModel);
setSfVoiceId((initialVoice.voiceKey || '').trim() || defaultVoiceKey);
setSfSpeed(initialVoice.speed ?? 1);
setSfGain(initialVoice.gain ?? 0);
@@ -325,21 +313,21 @@ const AddVoiceModal: React.FC<{
return;
}
const resolvedSiliconflowVoiceKey = (() => {
const resolvedVoiceKey = (() => {
const current = (sfVoiceId || '').trim();
if (current) return current;
return buildSiliconflowVoiceKey(initialVoice?.id || name, sfModel || SILICONFLOW_DEFAULT_MODEL);
return buildOpenAICompatibleVoiceKey(initialVoice?.id || name, openaiCompatibleModel || OPENAI_COMPATIBLE_DEFAULT_MODEL);
})();
const newVoice: Voice = {
id: initialVoice?.id || `${vendor === '硅基流动' ? 'sf' : 'gen'}-${Date.now()}`,
id: initialVoice?.id || `oa-${Date.now()}`,
name,
vendor,
gender,
language,
description: description || (vendor === '硅基流动' ? `Model: ${sfModel}` : `Model: ${model}`),
model: vendor === '硅基流动' ? sfModel : model,
voiceKey: vendor === '硅基流动' ? resolvedSiliconflowVoiceKey : voiceKey,
description: description || `Model: ${openaiCompatibleModel}`,
model: openaiCompatibleModel,
voiceKey: resolvedVoiceKey,
apiKey,
baseUrl,
speed: sfSpeed,
@@ -351,10 +339,8 @@ const AddVoiceModal: React.FC<{
setIsSaving(true);
await onSuccess(newVoice);
setName('');
setVendor('硅基流动');
setVendor('OpenAI Compatible');
setDescription('');
setModel('');
setVoiceKey('');
setApiKey('');
setBaseUrl('');
} catch (error: any) {
@@ -381,19 +367,7 @@ const AddVoiceModal: React.FC<{
<div className="space-y-4 max-h-[75vh] overflow-y-auto px-1 custom-scrollbar">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"> (Vendor)</label>
<div className="relative">
<select
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
value={vendor}
onChange={(e) => setVendor(e.target.value as any)}
>
<option value="硅基流动"> (SiliconFlow)</option>
<option value="Ali"> (Ali)</option>
<option value="Volcano"> (Volcano)</option>
<option value="Minimax">Minimax</option>
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
</div>
<Input value={vendor} readOnly className="h-10 border border-white/10 bg-white/5" />
</div>
<div className="h-px bg-white/5"></div>
@@ -403,15 +377,14 @@ const AddVoiceModal: React.FC<{
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="例如: 客服小美" />
</div>
{vendor === '硅基流动' ? (
<div className="space-y-4 animate-in fade-in slide-in-from-top-1 duration-200">
<div className="space-y-4 animate-in fade-in slide-in-from-top-1 duration-200">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"> (Model)</label>
<Input
className="font-mono text-xs"
value={sfModel}
onChange={(e) => setSfModel(e.target.value)}
value={openaiCompatibleModel}
onChange={(e) => setOpenaiCompatibleModel(e.target.value)}
placeholder="例如: FunAudioLLM/CosyVoice2-0.5B"
/>
</div>
@@ -445,20 +418,6 @@ const AddVoiceModal: React.FC<{
</div>
</div>
</div>
) : (
<div className="space-y-4 animate-in fade-in slide-in-from-top-1 duration-200">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"></label>
<Input value={model} onChange={(e) => setModel(e.target.value)} placeholder="API Model Key" />
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"></label>
<Input value={voiceKey} onChange={(e) => setVoiceKey(e.target.value)} placeholder="Voice Key" />
</div>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
@@ -560,7 +519,7 @@ const CloneVoiceModal: React.FC<{
const newVoice: Voice = {
id: `v-${Date.now()}`,
name,
vendor: 'Volcano',
vendor: 'OpenAI Compatible',
gender: 'Female',
language: 'zh',
description: description || 'User cloned voice',