From 1afc3b4011b6a747a00ce1a203231141168f00a0 Mon Sep 17 00:00:00 2001
From: LauraGPT <199134975+LauraGPT@users.noreply.github.com>
Date: Tue, 4 Aug 2026 01:13:17 +0000
Subject: [PATCH] feat(asr): add local FunASR provider
Signed-off-by: LauraGPT <199134975+LauraGPT@users.noreply.github.com>
---
.env.example | 3 +
README-zh.md | 26 ++-
README.md | 26 ++-
components/settings/asr-settings.tsx | 1 +
lib/audio/asr-providers.ts | 26 ++-
lib/audio/constants.ts | 16 ++
lib/audio/provider-display.ts | 1 +
lib/audio/types.ts | 1 +
lib/audio/wav-utils.ts | 2 +-
lib/i18n/locales/ar-SA.json | 1 +
lib/i18n/locales/en-US.json | 1 +
lib/i18n/locales/es-MX.json | 1 +
lib/i18n/locales/ja-JP.json | 1 +
lib/i18n/locales/ko-KR.json | 1 +
lib/i18n/locales/pt-BR.json | 1 +
lib/i18n/locales/ru-RU.json | 1 +
lib/i18n/locales/zh-CN.json | 1 +
lib/i18n/locales/zh-TW.json | 1 +
lib/server/provider-config.ts | 3 +-
lib/store/settings.ts | 1 +
.../docs/content/docs/configuration.ar.mdx | 5 +
.../docs/content/docs/configuration.ja.mdx | 5 +
packages/docs/content/docs/configuration.mdx | 5 +
.../docs/content/docs/configuration.ru.mdx | 5 +
.../docs/content/docs/configuration.zh-cn.mdx | 5 +
.../docs/content/docs/configuration.zh-tw.mdx | 5 +
.../docs/content/docs/supported-models.ar.mdx | 1 +
.../docs/content/docs/supported-models.ja.mdx | 1 +
.../docs/content/docs/supported-models.mdx | 1 +
.../docs/content/docs/supported-models.ru.mdx | 1 +
.../content/docs/supported-models.zh-cn.mdx | 1 +
.../content/docs/supported-models.zh-tw.mdx | 1 +
public/logos/funasr.png | Bin 0 -> 3334 bytes
tests/audio/funasr-asr.test.ts | 152 ++++++++++++++++++
tests/audio/wav-utils.test.ts | 9 +-
tests/server/provider-config.test.ts | 24 +++
tests/store/settings-server-sync.test.ts | 9 ++
37 files changed, 330 insertions(+), 15 deletions(-)
create mode 100644 public/logos/funasr.png
create mode 100644 tests/audio/funasr-asr.test.ts
diff --git a/.env.example b/.env.example
index 07bb50cd6b..1c406a200d 100644
--- a/.env.example
+++ b/.env.example
@@ -131,6 +131,9 @@ ASR_QWEN_BASE_URL=
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR (local, WAV input only, no API key required)
+# ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR (local, WAV input only, no API key required)
# ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
diff --git a/README-zh.md b/README-zh.md
index cc4e45e293..8e54546256 100644
--- a/README-zh.md
+++ b/README-zh.md
@@ -33,7 +33,7 @@
English | 简体中文
- 在线体验 · 快速开始 · Lemonade · 功能特性 · 使用场景 · OpenClaw
+ 在线体验 · 快速开始 · Lemonade · FunASR · 功能特性 · 使用场景 · OpenClaw
@@ -132,7 +132,7 @@ providers:
apiKey: sk-ant-...
```
-支持的服务商:**OpenAI**、**Azure OpenAI**、**Anthropic**、**Google Gemini**、**DeepSeek**、**通义千问 Qwen**、**Kimi**、**MiniMax**、**Grok (xAI)**、**OpenRouter**、**豆包**、**腾讯混元 / TokenHub**、**小米 MiMo**、**智谱 GLM**、**Ollama**(本地)、**Lemonade**(本地 LLM / 图像 / TTS / ASR)以及任何兼容 OpenAI API 的服务。
+支持的服务商:**OpenAI**、**Azure OpenAI**、**Anthropic**、**Google Gemini**、**DeepSeek**、**通义千问 Qwen**、**Kimi**、**MiniMax**、**Grok (xAI)**、**OpenRouter**、**豆包**、**腾讯混元 / TokenHub**、**小米 MiMo**、**智谱 GLM**、**Ollama**(本地)、**Lemonade**(本地 LLM / 图像 / TTS / ASR)、**FunASR**(本地 ASR)以及任何兼容 OpenAI API 的服务。
@@ -149,6 +149,28 @@ ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
IMAGE_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+
+
+### 可选:FunASR(本地语音识别)
+
+OpenMAIC 可以通过 FunASR 的 OpenAI 兼容服务完成本地转写。内置 provider 支持 SenseVoiceSmall、Paraformer 和 Fun-ASR-Nano,无需 API Key。
+
+```bash
+python -m pip install torch torchaudio
+python -m pip install "funasr==1.4.0" fastapi uvicorn python-multipart
+# NVIDIA GPU 上运行 Fun-ASR-Nano 时再安装 vLLM
+python -m pip install vllm
+funasr-server --device cuda --model fun-asr-nano
+```
+
+将 OpenMAIC 指向该服务:
+
+```env
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+```
+
+纯 CPU 环境可运行 `funasr-server --device cpu --model sensevoice`。生产部署方式参见 [FunASR 部署指南](https://github.com/modelscope/FunASR#deploy)。
+
OpenAI 快速示例:
```env
diff --git a/README.md b/README.md
index a0c7e7352d..6ee3ac2148 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@
English | 简体中文
- Live Demo · Quick Start · Lemonade · Features · Use Cases · OpenClaw
+ Live Demo · Quick Start · Lemonade · FunASR · Features · Use Cases · OpenClaw
@@ -132,7 +132,7 @@ providers:
apiKey: sk-ant-...
```
-Supported providers: **OpenAI**, **Azure OpenAI**, **Anthropic**, **Google Gemini**, **DeepSeek**, **Qwen**, **Kimi**, **MiniMax**, **Grok (xAI)**, **OpenRouter**, **Doubao**, **Tencent Hunyuan/TokenHub**, **Xiaomi MiMo**, **GLM (Zhipu)**, **Ollama** (local), **Lemonade** (local LLM / image / TTS / ASR), and any OpenAI-compatible API.
+Supported providers: **OpenAI**, **Azure OpenAI**, **Anthropic**, **Google Gemini**, **DeepSeek**, **Qwen**, **Kimi**, **MiniMax**, **Grok (xAI)**, **OpenRouter**, **Doubao**, **Tencent Hunyuan/TokenHub**, **Xiaomi MiMo**, **GLM (Zhipu)**, **Ollama** (local), **Lemonade** (local LLM / image / TTS / ASR), **FunASR** (local ASR), and any OpenAI-compatible API.
@@ -149,6 +149,28 @@ ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
IMAGE_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+
+
+### Optional: FunASR (Local Speech Recognition)
+
+OpenMAIC can transcribe locally through FunASR's OpenAI-compatible server. The built-in provider supports SenseVoiceSmall, Paraformer, and Fun-ASR-Nano and requires no API key.
+
+```bash
+python -m pip install torch torchaudio
+python -m pip install "funasr==1.4.0" fastapi uvicorn python-multipart
+# Add vLLM for Fun-ASR-Nano on NVIDIA GPUs
+python -m pip install vllm
+funasr-server --device cuda --model fun-asr-nano
+```
+
+Point OpenMAIC at the server:
+
+```env
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+```
+
+Use `funasr-server --device cpu --model sensevoice` for a CPU-only setup. See the [FunASR deployment guide](https://github.com/modelscope/FunASR#deploy) for production options.
+
OpenAI quick example:
```env
diff --git a/components/settings/asr-settings.tsx b/components/settings/asr-settings.tsx
index dec386aae6..7e30c1abab 100644
--- a/components/settings/asr-settings.tsx
+++ b/components/settings/asr-settings.tsx
@@ -281,6 +281,7 @@ export function ASRSettings({ selectedProviderId }: ASRSettingsProps) {
} else {
switch (selectedProviderId) {
case 'openai-whisper':
+ case 'funasr-asr':
case 'lemonade-asr':
endpointPath = '/audio/transcriptions';
break;
diff --git a/lib/audio/asr-providers.ts b/lib/audio/asr-providers.ts
index cb2a3ba9f2..09920ad096 100644
--- a/lib/audio/asr-providers.ts
+++ b/lib/audio/asr-providers.ts
@@ -185,8 +185,16 @@ export async function transcribeAudio(
case 'azure-asr':
return await transcribeAzureASR(config, audioBuffer);
+ case 'funasr-asr':
+ return await transcribeWavOpenAICompatibleASR(config, audioBuffer, 'funasr-asr', 'FunASR');
+
case 'lemonade-asr':
- return await transcribeLemonadeASR(config, audioBuffer);
+ return await transcribeWavOpenAICompatibleASR(
+ config,
+ audioBuffer,
+ 'lemonade-asr',
+ 'Lemonade',
+ );
default:
if (isCustomASRProvider(config.providerId)) {
@@ -197,15 +205,17 @@ export async function transcribeAudio(
}
/**
- * Lemonade ASR implementation (OpenAI-compatible multipart transcription).
+ * WAV-only OpenAI-compatible multipart transcription.
*
- * Lemonade currently supports WAV input and JSON response format.
+ * Used by local providers whose transcription endpoint accepts WAV and JSON.
*/
-async function transcribeLemonadeASR(
+async function transcribeWavOpenAICompatibleASR(
config: ASRModelConfig,
audioBuffer: Buffer | Blob,
+ providerId: 'funasr-asr' | 'lemonade-asr',
+ providerName: string,
): Promise {
- const baseUrl = (config.baseUrl || ASR_PROVIDERS['lemonade-asr'].defaultBaseUrl || '').replace(
+ const baseUrl = (config.baseUrl || ASR_PROVIDERS[providerId].defaultBaseUrl || '').replace(
/\/$/,
'',
);
@@ -213,13 +223,13 @@ async function transcribeLemonadeASR(
const audioBlob = await toAudioBlob(audioBuffer);
if (!(await isWavAudio(audioBlob))) {
throw new Error(
- 'Lemonade ASR currently supports WAV input only. Recordings should be converted to WAV before upload.',
+ `${providerName} ASR currently supports WAV input only. Recordings should be converted to WAV before upload.`,
);
}
const formData = new FormData();
formData.set('file', audioBlob, 'audio.wav');
- formData.set('model', config.modelId || ASR_PROVIDERS['lemonade-asr'].defaultModelId);
+ formData.set('model', config.modelId || ASR_PROVIDERS[providerId].defaultModelId);
formData.set('response_format', 'json');
if (config.language && config.language !== 'auto') {
formData.set('language', config.language);
@@ -236,7 +246,7 @@ async function transcribeLemonadeASR(
if (errorText.includes('audio is empty') || errorText.includes('too short')) {
return { text: '' };
}
- throw new Error(`Lemonade ASR API error: ${errorText || response.statusText}`);
+ throw new Error(`${providerName} ASR API error: ${errorText || response.statusText}`);
}
const data = await response.json();
diff --git a/lib/audio/constants.ts b/lib/audio/constants.ts
index 0bea9fa916..64556d8f9a 100644
--- a/lib/audio/constants.ts
+++ b/lib/audio/constants.ts
@@ -1250,6 +1250,22 @@ export const ASR_PROVIDERS: Record = {
supportedFormats: ['webm'], // MediaRecorder format
},
+ 'funasr-asr': {
+ id: 'funasr-asr',
+ name: 'FunASR',
+ requiresApiKey: false,
+ defaultBaseUrl: 'http://localhost:8000/v1',
+ icon: '/logos/funasr.png',
+ models: [
+ { id: 'sensevoice', name: 'SenseVoiceSmall' },
+ { id: 'paraformer', name: 'Paraformer' },
+ { id: 'fun-asr-nano', name: 'Fun-ASR-Nano' },
+ ],
+ defaultModelId: 'sensevoice',
+ supportedLanguages: ['auto', 'zh', 'en', 'ja', 'ko', 'yue'],
+ supportedFormats: ['wav'],
+ },
+
'lemonade-asr': {
id: 'lemonade-asr',
name: 'Lemonade ASR',
diff --git a/lib/audio/provider-display.ts b/lib/audio/provider-display.ts
index b1fe846e93..181a8d86af 100644
--- a/lib/audio/provider-display.ts
+++ b/lib/audio/provider-display.ts
@@ -18,6 +18,7 @@ const ASR_PROVIDER_NAME_KEYS: Record = {
'browser-native': 'settings.providerBrowserNative',
'qwen-asr': 'settings.providerQwenASR',
'azure-asr': 'settings.providerAzureASR',
+ 'funasr-asr': 'settings.providerFunASRASR',
'lemonade-asr': 'settings.providerLemonadeASR',
};
diff --git a/lib/audio/types.ts b/lib/audio/types.ts
index 9bce9b1f7b..b3f0792c1e 100644
--- a/lib/audio/types.ts
+++ b/lib/audio/types.ts
@@ -157,6 +157,7 @@ export type BuiltInASRProviderId =
| 'openai-whisper'
| 'browser-native'
| 'qwen-asr'
+ | 'funasr-asr'
| 'lemonade-asr'
| 'azure-asr';
diff --git a/lib/audio/wav-utils.ts b/lib/audio/wav-utils.ts
index 1a81a37f97..e1c95a9551 100644
--- a/lib/audio/wav-utils.ts
+++ b/lib/audio/wav-utils.ts
@@ -77,7 +77,7 @@ export async function normalizeASRUploadAudio(
providerId: string,
audioBlob: Blob,
): Promise<{ blob: Blob; fileName: string }> {
- if (providerId !== 'lemonade-asr') {
+ if (providerId !== 'lemonade-asr' && providerId !== 'funasr-asr') {
return { blob: audioBlob, fileName: 'recording.webm' };
}
return { blob: await audioBlobToWav(audioBlob), fileName: 'recording.wav' };
diff --git a/lib/i18n/locales/ar-SA.json b/lib/i18n/locales/ar-SA.json
index eab088e82c..1526851a37 100644
--- a/lib/i18n/locales/ar-SA.json
+++ b/lib/i18n/locales/ar-SA.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "التعرّف على الكلام المدمج في المتصفح",
"providerQwenASR": "Qwen ASR (سحابة علي بابا بايليان)",
+ "providerFunASRASR": "FunASR (محلي)",
"providerLemonadeASR": "Lemonade ASR (محلي)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf (مُدمج)",
diff --git a/lib/i18n/locales/en-US.json b/lib/i18n/locales/en-US.json
index 3957adf035..664597dd1a 100644
--- a/lib/i18n/locales/en-US.json
+++ b/lib/i18n/locales/en-US.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "Browser Native ASR",
"providerQwenASR": "Qwen ASR (Alibaba Cloud Bailian)",
+ "providerFunASRASR": "FunASR (Local)",
"providerLemonadeASR": "Lemonade ASR (Local)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf (Built-in)",
diff --git a/lib/i18n/locales/es-MX.json b/lib/i18n/locales/es-MX.json
index 5a98432f50..c8aa3576cf 100644
--- a/lib/i18n/locales/es-MX.json
+++ b/lib/i18n/locales/es-MX.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "ASR nativo del navegador",
"providerQwenASR": "Qwen ASR (Alibaba Cloud Bailian)",
+ "providerFunASRASR": "FunASR (local)",
"providerLemonadeASR": "Lemonade ASR (local)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf (integrado)",
diff --git a/lib/i18n/locales/ja-JP.json b/lib/i18n/locales/ja-JP.json
index 02099c60d9..d65823654b 100644
--- a/lib/i18n/locales/ja-JP.json
+++ b/lib/i18n/locales/ja-JP.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "ブラウザネイティブASR",
"providerQwenASR": "Qwen ASR(Alibaba Cloud百錬)",
+ "providerFunASRASR": "FunASR(ローカル)",
"providerLemonadeASR": "Lemonade ASR(ローカル)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf(組み込み)",
diff --git a/lib/i18n/locales/ko-KR.json b/lib/i18n/locales/ko-KR.json
index c36f44c83b..73184cf48a 100644
--- a/lib/i18n/locales/ko-KR.json
+++ b/lib/i18n/locales/ko-KR.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "브라우저 기본 ASR",
"providerQwenASR": "Qwen ASR (Alibaba Cloud Bailian)",
+ "providerFunASRASR": "FunASR (로컬)",
"providerLemonadeASR": "Lemonade ASR (로컬)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf (내장)",
diff --git a/lib/i18n/locales/pt-BR.json b/lib/i18n/locales/pt-BR.json
index c64afb736f..5ba550237c 100644
--- a/lib/i18n/locales/pt-BR.json
+++ b/lib/i18n/locales/pt-BR.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "ASR Nativo do Navegador",
"providerQwenASR": "Qwen ASR (Alibaba Cloud Bailian)",
+ "providerFunASRASR": "FunASR (Local)",
"providerLemonadeASR": "Lemonade ASR (Local)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf (Nativo)",
diff --git a/lib/i18n/locales/ru-RU.json b/lib/i18n/locales/ru-RU.json
index 45daf3fd12..94d6eef606 100644
--- a/lib/i18n/locales/ru-RU.json
+++ b/lib/i18n/locales/ru-RU.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "Встроенный ASR браузера",
"providerQwenASR": "Qwen ASR (Alibaba Cloud Bailian)",
+ "providerFunASRASR": "FunASR (Локальный)",
"providerLemonadeASR": "Lemonade ASR (Локальный)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf (встроенный)",
diff --git a/lib/i18n/locales/zh-CN.json b/lib/i18n/locales/zh-CN.json
index c05dbc2a72..b357fe3dfd 100644
--- a/lib/i18n/locales/zh-CN.json
+++ b/lib/i18n/locales/zh-CN.json
@@ -1312,6 +1312,7 @@
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "浏览器原生 ASR",
"providerQwenASR": "Qwen ASR(阿里云百炼)",
+ "providerFunASRASR": "FunASR(本地)",
"providerLemonadeASR": "Lemonade ASR(本地)",
"providerAzureASR": "Azure STT",
"providerUnpdf": "unpdf(内置)",
diff --git a/lib/i18n/locales/zh-TW.json b/lib/i18n/locales/zh-TW.json
index 0927c9d0e6..f8d80831db 100644
--- a/lib/i18n/locales/zh-TW.json
+++ b/lib/i18n/locales/zh-TW.json
@@ -1245,6 +1245,7 @@
"providerBrowserNativeTTS": "瀏覽器原生 TTS",
"providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)",
"providerBrowserNative": "瀏覽器原生 ASR",
+ "providerFunASRASR": "FunASR(本機)",
"providerLemonadeASR": "Lemonade ASR(本機)",
"providerAzureASR": "Azure STT(語音轉文字)",
"providerQwenASR": "Qwen ASR(阿里雲百煉)",
diff --git a/lib/server/provider-config.ts b/lib/server/provider-config.ts
index 941bab808f..2db557db52 100644
--- a/lib/server/provider-config.ts
+++ b/lib/server/provider-config.ts
@@ -98,6 +98,7 @@ const ASR_ENV_MAP: Record = {
ASR_OPENAI: 'openai-whisper',
ASR_QWEN: 'qwen-asr',
ASR_AZURE: 'azure-asr',
+ ASR_FUNASR: 'funasr-asr',
ASR_LEMONADE: 'lemonade-asr',
};
@@ -376,7 +377,7 @@ function buildConfig(yamlData: YamlData): ServerConfig {
keylessProviders: new Set(['voxcpm-tts', 'lemonade-tts']),
}),
asr: loadEnvSection(ASR_ENV_MAP, yamlData.asr, {
- keylessProviders: new Set(['lemonade-asr']),
+ keylessProviders: new Set(['funasr-asr', 'lemonade-asr']),
}),
pdf: applyAliDocMindFallback(
loadEnvSection(PDF_ENV_MAP, yamlData.pdf, {
diff --git a/lib/store/settings.ts b/lib/store/settings.ts
index 4e9d52fdde..ad8ba7f612 100644
--- a/lib/store/settings.ts
+++ b/lib/store/settings.ts
@@ -501,6 +501,7 @@ const getDefaultAudioConfig = () => ({
'browser-native': { apiKey: '', baseUrl: '', enabled: true },
'qwen-asr': { apiKey: '', baseUrl: '', enabled: false },
'azure-asr': { apiKey: '', baseUrl: '', enabled: false },
+ 'funasr-asr': { apiKey: '', baseUrl: '', enabled: false },
'lemonade-asr': { apiKey: '', baseUrl: '', enabled: false },
} as Record,
});
diff --git a/packages/docs/content/docs/configuration.ar.mdx b/packages/docs/content/docs/configuration.ar.mdx
index 14b5257c9b..7d14758ee8 100644
--- a/packages/docs/content/docs/configuration.ar.mdx
+++ b/packages/docs/content/docs/configuration.ar.mdx
@@ -104,10 +104,15 @@ ASR_QWEN_BASE_URL= # تجاوز اختياري
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR (محلي، لا يحتاج إلى مفتاح)
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR (محلي، لا يحتاج إلى مفتاح)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+شغّل خدمة FunASR المتوافقة مع OpenAI باستخدام `funasr-server --device cuda`. تستقبل ملفات WAV على `/v1/audio/transcriptions` وتدعم SenseVoiceSmall وParaformer وFun-ASR-Nano.
+
ASR المدمج في المتصفح لا يحتاج إلى إعدادات خادم.
يمكن أيضًا إضافة موفري ASR مخصصين متوافقين مع OpenAI من الإعدادات، مع إدخال عنوان URL الأساسي، النموذج، واللغات المدعومة. تحفظ هذه الإعدادات في إعدادات العميل.
diff --git a/packages/docs/content/docs/configuration.ja.mdx b/packages/docs/content/docs/configuration.ja.mdx
index 52ca8f3a26..530dd2ea60 100644
--- a/packages/docs/content/docs/configuration.ja.mdx
+++ b/packages/docs/content/docs/configuration.ja.mdx
@@ -104,10 +104,15 @@ ASR_QWEN_BASE_URL= # 任意の上書き
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR(ローカル、key 不要)
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR(ローカル、key 不要)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+`funasr-server --device cuda` で OpenAI 互換サービスを起動します。`/v1/audio/transcriptions` で WAV を受け付け、モデル選択から SenseVoiceSmall、Paraformer、Fun-ASR-Nano を利用できます。
+
ブラウザのネイティブ ASR はサーバー設定不要です。
カスタム OpenAI 互換 ASR provider も設定画面から追加可能で、Base URL、モデル、対応言語を指定します。これらはクライアント設定に保存されます。
diff --git a/packages/docs/content/docs/configuration.mdx b/packages/docs/content/docs/configuration.mdx
index c0a52aeab9..1f8313d9ec 100644
--- a/packages/docs/content/docs/configuration.mdx
+++ b/packages/docs/content/docs/configuration.mdx
@@ -104,10 +104,15 @@ ASR_QWEN_BASE_URL= # optional override
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR (local, no key required)
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR (local, no key required)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+Start the OpenAI-compatible FunASR service with `funasr-server --device cuda`. It accepts WAV uploads at `/v1/audio/transcriptions` and provides SenseVoiceSmall, Paraformer, and Fun-ASR-Nano through the model selector.
+
Browser-native ASR requires no server configuration.
You can also add a custom OpenAI-compatible ASR provider in Settings by entering its Base URL, model, and supported languages; this configuration is stored in client settings.
diff --git a/packages/docs/content/docs/configuration.ru.mdx b/packages/docs/content/docs/configuration.ru.mdx
index 0c1fa83366..5980303361 100644
--- a/packages/docs/content/docs/configuration.ru.mdx
+++ b/packages/docs/content/docs/configuration.ru.mdx
@@ -104,10 +104,15 @@ ASR_QWEN_BASE_URL= # опциональное переопределен
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR (локально, key не нужен)
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR (локально, key не нужен)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+Запустите OpenAI-совместимый сервис командой `funasr-server --device cuda`. Он принимает WAV на `/v1/audio/transcriptions` и поддерживает SenseVoiceSmall, Paraformer и Fun-ASR-Nano.
+
Встроенный в браузер ASR не требует серверной настройки.
Кастомные OpenAI-совместимые ASR провайдеры можно добавить в настройках, указав Base URL, модели и поддерживаемые языки. Эти настройки сохраняются в клиенте.
diff --git a/packages/docs/content/docs/configuration.zh-cn.mdx b/packages/docs/content/docs/configuration.zh-cn.mdx
index 31bba03568..b00a50e0e6 100644
--- a/packages/docs/content/docs/configuration.zh-cn.mdx
+++ b/packages/docs/content/docs/configuration.zh-cn.mdx
@@ -104,10 +104,15 @@ ASR_QWEN_BASE_URL= # 可选覆盖
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR(本地,不需要 key)
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR(本地,不需要 key)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+使用 `funasr-server --device cuda` 启动 OpenAI 兼容服务。它在 `/v1/audio/transcriptions` 接收 WAV,并可在模型选择器中使用 SenseVoiceSmall、Paraformer 和 Fun-ASR-Nano。
+
浏览器原生 ASR 不需要服务端配置。
也可以在设置中添加自定义 OpenAI 兼容 ASR provider,填写 Base URL、模型和支持语言;该配置保存在客户端设置中。
diff --git a/packages/docs/content/docs/configuration.zh-tw.mdx b/packages/docs/content/docs/configuration.zh-tw.mdx
index 9f25faa86f..502c13cd32 100644
--- a/packages/docs/content/docs/configuration.zh-tw.mdx
+++ b/packages/docs/content/docs/configuration.zh-tw.mdx
@@ -104,10 +104,15 @@ ASR_QWEN_BASE_URL= # 選填覆寫值
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
+# FunASR(本機,不需要 key)
+ASR_FUNASR_BASE_URL=http://localhost:8000/v1
+
# Lemonade ASR(本機,不需要 key)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
```
+使用 `funasr-server --device cuda` 啟動 OpenAI 相容服務。它在 `/v1/audio/transcriptions` 接收 WAV,並可在模型選擇器中使用 SenseVoiceSmall、Paraformer 和 Fun-ASR-Nano。
+
瀏覽器原生 ASR 不需要伺服器端設定。
也可以在設定中新增自訂 OpenAI 相容 ASR provider,填寫 Base URL、模型和支援語言;此設定會儲存在用戶端設定中。
diff --git a/packages/docs/content/docs/supported-models.ar.mdx b/packages/docs/content/docs/supported-models.ar.mdx
index cfbb0e501d..158a4f74c7 100644
--- a/packages/docs/content/docs/supported-models.ar.mdx
+++ b/packages/docs/content/docs/supported-models.ar.mdx
@@ -78,6 +78,7 @@ description: قائمة النماذج المدمجة ومزودي الخدمة
| OpenAI Whisper | `openai-whisper` | GPT-4o Mini Transcribe (`gpt-4o-mini-transcribe`)
GPT-4o Transcribe (`gpt-4o-transcribe`)
Whisper-1 (`whisper-1`) |
| Qwen ASR | `qwen-asr` | Qwen3 ASR Flash (`qwen3-asr-flash`) |
| Azure STT | `azure-asr` | لا يوجد محدد نموذج؛ تُستخدم Azure Speech to Text. |
+| FunASR | `funasr-asr` | SenseVoiceSmall (`sensevoice`)
Paraformer (`paraformer`)
Fun-ASR-Nano (`fun-asr-nano`) |
| Lemonade ASR | `lemonade-asr` | Whisper Base (`Whisper-Base`)
Whisper Large v3 (`Whisper-Large-v3`)
Whisper Large v3 Turbo (`Whisper-Large-v3-Turbo`)
Whisper Medium (`Whisper-Medium`)
Whisper Small (`Whisper-Small`)
Whisper Tiny (`Whisper-Tiny`) |
| Browser Native ASR | `browser-native` | لا يوجد محدد نموذج؛ تُستخدم Web Speech API للمتصفح. |
diff --git a/packages/docs/content/docs/supported-models.ja.mdx b/packages/docs/content/docs/supported-models.ja.mdx
index 43fbfa54c4..34680bd77b 100644
--- a/packages/docs/content/docs/supported-models.ja.mdx
+++ b/packages/docs/content/docs/supported-models.ja.mdx
@@ -78,6 +78,7 @@ description: オープンソース版 OpenMAIC の組み込みモデルと provi
| OpenAI Whisper | `openai-whisper` | GPT-4o Mini Transcribe (`gpt-4o-mini-transcribe`)
GPT-4o Transcribe (`gpt-4o-transcribe`)
Whisper-1 (`whisper-1`) |
| Qwen ASR | `qwen-asr` | Qwen3 ASR Flash (`qwen3-asr-flash`) |
| Azure STT | `azure-asr` | モデルセレクターはなく、Azure Speech to Text を使用します。 |
+| FunASR | `funasr-asr` | SenseVoiceSmall (`sensevoice`)
Paraformer (`paraformer`)
Fun-ASR-Nano (`fun-asr-nano`) |
| Lemonade ASR | `lemonade-asr` | Whisper Base (`Whisper-Base`)
Whisper Large v3 (`Whisper-Large-v3`)
Whisper Large v3 Turbo (`Whisper-Large-v3-Turbo`)
Whisper Medium (`Whisper-Medium`)
Whisper Small (`Whisper-Small`)
Whisper Tiny (`Whisper-Tiny`) |
| ブラウザネイティブ ASR | `browser-native` | モデルセレクターはなく、ブラウザの Web Speech API を使用します。 |
diff --git a/packages/docs/content/docs/supported-models.mdx b/packages/docs/content/docs/supported-models.mdx
index 9db2c3a5a5..552631178a 100644
--- a/packages/docs/content/docs/supported-models.mdx
+++ b/packages/docs/content/docs/supported-models.mdx
@@ -78,6 +78,7 @@ This table lists model selectors and self-hosted backends. It does not enumerate
| OpenAI Whisper | `openai-whisper` | GPT-4o Mini Transcribe (`gpt-4o-mini-transcribe`)
GPT-4o Transcribe (`gpt-4o-transcribe`)
Whisper-1 (`whisper-1`) |
| Qwen ASR | `qwen-asr` | Qwen3 ASR Flash (`qwen3-asr-flash`) |
| Azure STT | `azure-asr` | No model selector. Uses Azure Speech to Text. |
+| FunASR | `funasr-asr` | SenseVoiceSmall (`sensevoice`)
Paraformer (`paraformer`)
Fun-ASR-Nano (`fun-asr-nano`) |
| Lemonade ASR | `lemonade-asr` | Whisper Base (`Whisper-Base`)
Whisper Large v3 (`Whisper-Large-v3`)
Whisper Large v3 Turbo (`Whisper-Large-v3-Turbo`)
Whisper Medium (`Whisper-Medium`)
Whisper Small (`Whisper-Small`)
Whisper Tiny (`Whisper-Tiny`) |
| Browser Native ASR | `browser-native` | No model selector. Uses the browser Web Speech API. |
diff --git a/packages/docs/content/docs/supported-models.ru.mdx b/packages/docs/content/docs/supported-models.ru.mdx
index a09eaa4a6c..fcbc6cd47f 100644
--- a/packages/docs/content/docs/supported-models.ru.mdx
+++ b/packages/docs/content/docs/supported-models.ru.mdx
@@ -78,6 +78,7 @@ description: Встроенные модели и список провайде
| OpenAI Whisper | `openai-whisper` | GPT-4o Mini Transcribe (`gpt-4o-mini-transcribe`)
GPT-4o Transcribe (`gpt-4o-transcribe`)
Whisper-1 (`whisper-1`) |
| Qwen ASR | `qwen-asr` | Qwen3 ASR Flash (`qwen3-asr-flash`) |
| Azure STT | `azure-asr` | Нет селектора моделей, используется Azure Speech to Text. |
+| FunASR | `funasr-asr` | SenseVoiceSmall (`sensevoice`)
Paraformer (`paraformer`)
Fun-ASR-Nano (`fun-asr-nano`) |
| Lemonade ASR | `lemonade-asr` | Whisper Base (`Whisper-Base`)
Whisper Large v3 (`Whisper-Large-v3`)
Whisper Large v3 Turbo (`Whisper-Large-v3-Turbo`)
Whisper Medium (`Whisper-Medium`)
Whisper Small (`Whisper-Small`)
Whisper Tiny (`Whisper-Tiny`) |
| Браузерный нативный ASR | `browser-native` | Нет селектора моделей, используется браузерный Web Speech API. |
diff --git a/packages/docs/content/docs/supported-models.zh-cn.mdx b/packages/docs/content/docs/supported-models.zh-cn.mdx
index e4285c4702..69b2fdfe72 100644
--- a/packages/docs/content/docs/supported-models.zh-cn.mdx
+++ b/packages/docs/content/docs/supported-models.zh-cn.mdx
@@ -78,6 +78,7 @@ description: 开源版 OpenMAIC 的内置模型与 provider 列表。
| OpenAI Whisper | `openai-whisper` | GPT-4o Mini Transcribe (`gpt-4o-mini-transcribe`)
GPT-4o Transcribe (`gpt-4o-transcribe`)
Whisper-1 (`whisper-1`) |
| Qwen ASR | `qwen-asr` | Qwen3 ASR Flash (`qwen3-asr-flash`) |
| Azure STT | `azure-asr` | 无模型选择器,使用 Azure Speech to Text。 |
+| FunASR | `funasr-asr` | SenseVoiceSmall (`sensevoice`)
Paraformer (`paraformer`)
Fun-ASR-Nano (`fun-asr-nano`) |
| Lemonade ASR | `lemonade-asr` | Whisper Base (`Whisper-Base`)
Whisper Large v3 (`Whisper-Large-v3`)
Whisper Large v3 Turbo (`Whisper-Large-v3-Turbo`)
Whisper Medium (`Whisper-Medium`)
Whisper Small (`Whisper-Small`)
Whisper Tiny (`Whisper-Tiny`) |
| 浏览器原生 ASR | `browser-native` | 无模型选择器,使用浏览器 Web Speech API。 |
diff --git a/packages/docs/content/docs/supported-models.zh-tw.mdx b/packages/docs/content/docs/supported-models.zh-tw.mdx
index e0be30529d..5ea774c168 100644
--- a/packages/docs/content/docs/supported-models.zh-tw.mdx
+++ b/packages/docs/content/docs/supported-models.zh-tw.mdx
@@ -78,6 +78,7 @@ description: 開源版 OpenMAIC 的內建模型與 provider 清單。
| OpenAI Whisper | `openai-whisper` | GPT-4o Mini Transcribe (`gpt-4o-mini-transcribe`)
GPT-4o Transcribe (`gpt-4o-transcribe`)
Whisper-1 (`whisper-1`) |
| Qwen ASR | `qwen-asr` | Qwen3 ASR Flash (`qwen3-asr-flash`) |
| Azure STT | `azure-asr` | 無模型選擇器,使用 Azure Speech to Text。 |
+| FunASR | `funasr-asr` | SenseVoiceSmall (`sensevoice`)
Paraformer (`paraformer`)
Fun-ASR-Nano (`fun-asr-nano`) |
| Lemonade ASR | `lemonade-asr` | Whisper Base (`Whisper-Base`)
Whisper Large v3 (`Whisper-Large-v3`)
Whisper Large v3 Turbo (`Whisper-Large-v3-Turbo`)
Whisper Medium (`Whisper-Medium`)
Whisper Small (`Whisper-Small`)
Whisper Tiny (`Whisper-Tiny`) |
| 瀏覽器原生 ASR | `browser-native` | 無模型選擇器,使用瀏覽器 Web Speech API。 |
diff --git a/public/logos/funasr.png b/public/logos/funasr.png
new file mode 100644
index 0000000000000000000000000000000000000000..96b2babfa6d6d6ad7edd4d10872e40c09aa9fead
GIT binary patch
literal 3334
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
LsIOkBYKQ#~+&ME7
literal 0
HcmV?d00001
diff --git a/tests/audio/funasr-asr.test.ts b/tests/audio/funasr-asr.test.ts
new file mode 100644
index 0000000000..77236205ea
--- /dev/null
+++ b/tests/audio/funasr-asr.test.ts
@@ -0,0 +1,152 @@
+import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
+import { transcribeAudio } from '@/lib/audio/asr-providers';
+import { ASR_PROVIDERS } from '@/lib/audio/constants';
+
+const mockFetch = vi.fn() as Mock;
+vi.stubGlobal('fetch', mockFetch);
+
+function wavBuffer(): Buffer {
+ const buf = Buffer.alloc(16);
+ buf.write('RIFF', 0, 'ascii');
+ buf.writeUInt32LE(8, 4);
+ buf.write('WAVE', 8, 'ascii');
+ return buf;
+}
+
+function wavArrayBuffer(): ArrayBuffer {
+ const buffer = wavBuffer();
+ const arrayBuffer = new ArrayBuffer(buffer.byteLength);
+ new Uint8Array(arrayBuffer).set(buffer);
+ return arrayBuffer;
+}
+
+describe('FunASR ASR', () => {
+ beforeEach(() => {
+ mockFetch.mockReset();
+ });
+
+ it('exposes the language hints documented by the FunASR CLI', () => {
+ expect(ASR_PROVIDERS['funasr-asr'].supportedLanguages).toEqual([
+ 'auto',
+ 'zh',
+ 'en',
+ 'ja',
+ 'ko',
+ 'yue',
+ ]);
+ });
+
+ it('posts WAV audio to /audio/transcriptions with an official FunASR model', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ text: 'hello' }),
+ });
+
+ const result = await transcribeAudio(
+ {
+ providerId: 'funasr-asr',
+ baseUrl: 'http://localhost:8000/v1/',
+ modelId: 'fun-asr-nano',
+ },
+ wavBuffer(),
+ );
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://localhost:8000/v1/audio/transcriptions',
+ expect.objectContaining({ method: 'POST', headers: {} }),
+ );
+ const formData = mockFetch.mock.calls[0][1].body as FormData;
+ expect(formData.get('model')).toBe('fun-asr-nano');
+ expect(formData.get('response_format')).toBe('json');
+ expect(formData.get('file')).toBeInstanceOf(Blob);
+ expect(result).toEqual({ text: 'hello' });
+ });
+
+ it('uses SenseVoice as the local CPU-friendly default model', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ text: 'ok' }),
+ });
+
+ await transcribeAudio({ providerId: 'funasr-asr' }, wavBuffer());
+ const formData = mockFetch.mock.calls[0][1].body as FormData;
+ expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:8000/v1/audio/transcriptions');
+ expect(formData.get('model')).toBe('sensevoice');
+ });
+
+ it('forwards an explicit language but not when set to "auto"', async () => {
+ mockFetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({ text: '' }),
+ });
+
+ await transcribeAudio({ providerId: 'funasr-asr', language: 'zh' }, wavBuffer());
+ let formData = mockFetch.mock.calls[0][1].body as FormData;
+ expect(formData.get('language')).toBe('zh');
+
+ mockFetch.mockClear();
+
+ await transcribeAudio({ providerId: 'funasr-asr', language: 'auto' }, wavBuffer());
+ formData = mockFetch.mock.calls[0][1].body as FormData;
+ expect(formData.get('language')).toBeNull();
+ });
+
+ it('adds bearer authentication only when an API key is configured', async () => {
+ mockFetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({ text: '' }),
+ });
+
+ await transcribeAudio({ providerId: 'funasr-asr', apiKey: ' local-secret ' }, wavBuffer());
+ expect(mockFetch.mock.calls[0][1].headers).toEqual({
+ Authorization: 'Bearer local-secret',
+ });
+ });
+
+ it('rejects non-WAV audio buffers', async () => {
+ const notWav = Buffer.from('IDXX' + '\0'.repeat(12));
+
+ await expect(transcribeAudio({ providerId: 'funasr-asr' }, notWav)).rejects.toThrow(
+ /WAV input only/,
+ );
+ expect(mockFetch).not.toHaveBeenCalled();
+ });
+
+ it('accepts WAV files even when the MIME type is missing', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ text: 'hello' }),
+ });
+
+ const audioFile = new File([wavArrayBuffer()], 'recording.wav');
+ const result = await transcribeAudio({ providerId: 'funasr-asr' }, audioFile);
+
+ expect(result).toEqual({ text: 'hello' });
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns empty text gracefully when upstream reports empty audio', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 400,
+ text: async () => 'audio is too short',
+ statusText: 'Bad Request',
+ });
+
+ const result = await transcribeAudio({ providerId: 'funasr-asr' }, wavBuffer());
+ expect(result).toEqual({ text: '' });
+ });
+
+ it('throws on unrecognized error payloads', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ text: async () => 'model crashed',
+ statusText: 'Internal Server Error',
+ });
+
+ await expect(transcribeAudio({ providerId: 'funasr-asr' }, wavBuffer())).rejects.toThrow(
+ /FunASR ASR API error.*model crashed/,
+ );
+ });
+});
diff --git a/tests/audio/wav-utils.test.ts b/tests/audio/wav-utils.test.ts
index cbddc46c9b..b196e21b89 100644
--- a/tests/audio/wav-utils.test.ts
+++ b/tests/audio/wav-utils.test.ts
@@ -26,7 +26,7 @@ describe('isWavBlob', () => {
});
describe('normalizeASRUploadAudio', () => {
- it('passes through non-lemonade providers unchanged', async () => {
+ it('passes through providers without WAV normalization unchanged', async () => {
const input = new Blob([new Uint8Array([1, 2, 3])], { type: 'audio/webm' });
const result = await normalizeASRUploadAudio('openai-whisper', input);
expect(result.blob).toBe(input);
@@ -39,4 +39,11 @@ describe('normalizeASRUploadAudio', () => {
expect(result.blob).toBe(input);
expect(result.fileName).toBe('recording.wav');
});
+
+ it('keeps WAV blobs unchanged for funasr-asr', async () => {
+ const input = new Blob([new Uint8Array([1, 2, 3])], { type: 'audio/wav' });
+ const result = await normalizeASRUploadAudio('funasr-asr', input);
+ expect(result.blob).toBe(input);
+ expect(result.fileName).toBe('recording.wav');
+ });
});
diff --git a/tests/server/provider-config.test.ts b/tests/server/provider-config.test.ts
index b40f84fe70..99f4ca9d0c 100644
--- a/tests/server/provider-config.test.ts
+++ b/tests/server/provider-config.test.ts
@@ -35,6 +35,7 @@ const ENV_PREFIXES_TO_CLEAR = [
'TTS_MINIMAX',
'ASR_OPENAI',
'ASR_QWEN',
+ 'ASR_FUNASR',
'PDF_UNPDF',
'PDF_MINERU',
'PDF_MINERU_CLOUD',
@@ -554,6 +555,29 @@ pdf:
});
});
+ describe('FunASR server configuration', () => {
+ it('activates the keyless provider from an env base URL', async () => {
+ vi.stubEnv('ASR_FUNASR_BASE_URL', 'http://localhost:8000/v1');
+ const { getServerASRProviders, resolveASRApiKey, resolveASRBaseUrl } =
+ await import('@/lib/server/provider-config');
+
+ expect(getServerASRProviders()['funasr-asr']).toEqual({});
+ expect(resolveASRApiKey('funasr-asr')).toBe('');
+ expect(resolveASRBaseUrl('funasr-asr')).toBe('http://localhost:8000/v1');
+ });
+
+ it('activates the keyless provider from YAML and keeps server config authoritative', async () => {
+ yamlOverride = 'asr:\n funasr-asr:\n baseUrl: http://funasr.internal:8000/v1\n';
+ const { getServerASRProviders, resolveASRBaseUrl } =
+ await import('@/lib/server/provider-config');
+
+ expect(getServerASRProviders()['funasr-asr']).toEqual({});
+ expect(resolveASRBaseUrl('funasr-asr', 'https://client.example.com/v1')).toBe(
+ 'http://funasr.internal:8000/v1',
+ );
+ });
+ });
+
describe('resolveManagedAliDocMindCredentials (AK/SK)', () => {
it('resolves YAML-managed AK/SK with NO ALIDOCMIND_* env vars', async () => {
// Regression: verification resolved YAML creds but extraction only had an
diff --git a/tests/store/settings-server-sync.test.ts b/tests/store/settings-server-sync.test.ts
index 156c8835d7..d85dc8e04b 100644
--- a/tests/store/settings-server-sync.test.ts
+++ b/tests/store/settings-server-sync.test.ts
@@ -1689,6 +1689,15 @@ describe('TTS provider enablement (#665)', () => {
expect(store.getState().ttsEnabled).toBe(false);
});
+ it('initializes the keyless FunASR provider on a fresh install', async () => {
+ const store = await getStore();
+ expect(store.getState().asrProvidersConfig['funasr-asr']).toEqual({
+ apiKey: '',
+ baseUrl: '',
+ enabled: false,
+ });
+ });
+
it('first server-sync auto-enables TTS when a server provider exists', async () => {
mockServerResponse({ tts: { 'openai-tts': {} } });
const store = await getStore();