Add DashScope ASR model support and enhance related components
- Introduced DashScope as a new ASR model in the database initialization. - Updated ASRModel schema to include vendor information. - Enhanced ASR router to support DashScope-specific functionality, including connection testing and preview capabilities. - Modified frontend components to accommodate DashScope as a selectable vendor with appropriate default settings. - Added tests to validate DashScope ASR model creation, updates, and connectivity. - Updated backend API to handle DashScope-specific base URLs and vendor normalization.
This commit is contained in:
@@ -82,6 +82,16 @@ const convertRecordedBlobToWav = async (blob: Blob): Promise<File> => {
|
||||
}
|
||||
};
|
||||
|
||||
const OPENAI_COMPATIBLE_DEFAULT_MODEL = 'FunAudioLLM/SenseVoiceSmall';
|
||||
const OPENAI_COMPATIBLE_DEFAULT_BASE_URL = 'https://api.siliconflow.cn/v1';
|
||||
const DASHSCOPE_DEFAULT_MODEL = 'qwen3-asr-flash-realtime';
|
||||
const DASHSCOPE_DEFAULT_BASE_URL = 'wss://dashscope.aliyuncs.com/api-ws/v1/realtime';
|
||||
|
||||
type ASRVendor = 'OpenAI Compatible' | 'DashScope';
|
||||
|
||||
const normalizeVendor = (value?: string): ASRVendor =>
|
||||
String(value || '').trim().toLowerCase() === 'dashscope' ? 'DashScope' : 'OpenAI Compatible';
|
||||
|
||||
export const ASRLibraryPage: React.FC = () => {
|
||||
const [models, setModels] = useState<ASRModel[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -271,10 +281,10 @@ const ASRModelModal: React.FC<{
|
||||
initialModel?: ASRModel;
|
||||
}> = ({ isOpen, onClose, onSubmit, initialModel }) => {
|
||||
const [name, setName] = useState('');
|
||||
const [vendor, setVendor] = useState('OpenAI Compatible');
|
||||
const [vendor, setVendor] = useState<ASRVendor>('OpenAI Compatible');
|
||||
const [language, setLanguage] = useState('zh');
|
||||
const [modelName, setModelName] = useState('FunAudioLLM/SenseVoiceSmall');
|
||||
const [baseUrl, setBaseUrl] = useState('https://api.siliconflow.cn/v1');
|
||||
const [modelName, setModelName] = useState(OPENAI_COMPATIBLE_DEFAULT_MODEL);
|
||||
const [baseUrl, setBaseUrl] = useState(OPENAI_COMPATIBLE_DEFAULT_BASE_URL);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [hotwords, setHotwords] = useState('');
|
||||
const [enablePunctuation, setEnablePunctuation] = useState(true);
|
||||
@@ -282,14 +292,40 @@ const ASRModelModal: React.FC<{
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const getDefaultModel = (nextVendor: ASRVendor): string =>
|
||||
nextVendor === 'DashScope' ? DASHSCOPE_DEFAULT_MODEL : OPENAI_COMPATIBLE_DEFAULT_MODEL;
|
||||
|
||||
const getDefaultBaseUrl = (nextVendor: ASRVendor): string =>
|
||||
nextVendor === 'DashScope' ? DASHSCOPE_DEFAULT_BASE_URL : OPENAI_COMPATIBLE_DEFAULT_BASE_URL;
|
||||
|
||||
const handleVendorChange = (nextVendor: ASRVendor) => {
|
||||
const previousVendor = vendor;
|
||||
setVendor(nextVendor);
|
||||
|
||||
const previousDefaultModel = getDefaultModel(previousVendor);
|
||||
const nextDefaultModel = getDefaultModel(nextVendor);
|
||||
const trimmedModelName = modelName.trim();
|
||||
if (!trimmedModelName || trimmedModelName === previousDefaultModel) {
|
||||
setModelName(nextDefaultModel);
|
||||
}
|
||||
|
||||
const previousDefaultBaseUrl = getDefaultBaseUrl(previousVendor);
|
||||
const nextDefaultBaseUrl = getDefaultBaseUrl(nextVendor);
|
||||
const trimmedBaseUrl = baseUrl.trim();
|
||||
if (!trimmedBaseUrl || trimmedBaseUrl === previousDefaultBaseUrl) {
|
||||
setBaseUrl(nextDefaultBaseUrl);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (initialModel) {
|
||||
const nextVendor = normalizeVendor(initialModel.vendor);
|
||||
setName(initialModel.name || '');
|
||||
setVendor(initialModel.vendor || 'OpenAI Compatible');
|
||||
setVendor(nextVendor);
|
||||
setLanguage(initialModel.language || 'zh');
|
||||
setModelName(initialModel.modelName || 'FunAudioLLM/SenseVoiceSmall');
|
||||
setBaseUrl(initialModel.baseUrl || 'https://api.siliconflow.cn/v1');
|
||||
setModelName(initialModel.modelName || getDefaultModel(nextVendor));
|
||||
setBaseUrl(initialModel.baseUrl || getDefaultBaseUrl(nextVendor));
|
||||
setApiKey(initialModel.apiKey || '');
|
||||
setHotwords(toHotwordsValue(initialModel.hotwords));
|
||||
setEnablePunctuation(initialModel.enablePunctuation ?? true);
|
||||
@@ -301,8 +337,8 @@ const ASRModelModal: React.FC<{
|
||||
setName('');
|
||||
setVendor('OpenAI Compatible');
|
||||
setLanguage('zh');
|
||||
setModelName('FunAudioLLM/SenseVoiceSmall');
|
||||
setBaseUrl('https://api.siliconflow.cn/v1');
|
||||
setModelName(OPENAI_COMPATIBLE_DEFAULT_MODEL);
|
||||
setBaseUrl(OPENAI_COMPATIBLE_DEFAULT_BASE_URL);
|
||||
setApiKey('');
|
||||
setHotwords('');
|
||||
setEnablePunctuation(true);
|
||||
@@ -368,9 +404,10 @@ const ASRModelModal: React.FC<{
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">接口类型</label>
|
||||
<Select
|
||||
value={vendor}
|
||||
onChange={(e) => setVendor(e.target.value)}
|
||||
onChange={(e) => handleVendorChange(e.target.value as ASRVendor)}
|
||||
>
|
||||
<option value="OpenAI Compatible">OpenAI Compatible</option>
|
||||
<option value="DashScope">DashScope</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -388,13 +425,22 @@ const ASRModelModal: React.FC<{
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Model Name</label>
|
||||
<Input value={modelName} onChange={(e) => setModelName(e.target.value)} placeholder="FunAudioLLM/SenseVoiceSmall" />
|
||||
<Input
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
placeholder={vendor === 'DashScope' ? DASHSCOPE_DEFAULT_MODEL : OPENAI_COMPATIBLE_DEFAULT_MODEL}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center"><Server className="w-3 h-3 mr-1.5" />Base URL</label>
|
||||
<Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="https://api.siliconflow.cn/v1" className="font-mono text-xs" />
|
||||
<Input
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder={vendor === 'DashScope' ? DASHSCOPE_DEFAULT_BASE_URL : OPENAI_COMPATIBLE_DEFAULT_BASE_URL}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center"><Key className="w-3 h-3 mr-1.5" />API Key</label>
|
||||
@@ -405,6 +451,11 @@ const ASRModelModal: React.FC<{
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">热词 (comma separated)</label>
|
||||
<Input value={hotwords} onChange={(e) => setHotwords(e.target.value)} placeholder="品牌名, 人名, 专有词" />
|
||||
{vendor === 'DashScope' && (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
DashScope 走实时 WebSocket ASR。预览建议使用浏览器录音或上传 WAV 文件。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
|
||||
Reference in New Issue
Block a user