diff --git a/.env.example b/.env.example
index 64467612ee..6c93d5b365 100644
--- a/.env.example
+++ b/.env.example
@@ -138,6 +138,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 86980992f5..05ae03dc07 100644
--- a/README-zh.md
+++ b/README-zh.md
@@ -33,7 +33,7 @@
English | 简体中文
- 在线体验 · 快速开始 · Lemonade · 功能特性 · 使用场景 · OpenClaw
+ 在线体验 · 快速开始 · Lemonade · FunASR · 功能特性 · 使用场景 · OpenClaw
@@ -137,7 +137,7 @@ providers:
- us.anthropic.claude-opus-4-8
```
-支持的服务商:**OpenAI**、**Azure OpenAI**、**Anthropic**、**Amazon Bedrock**、**Google Gemini**、**DeepSeek**、**通义千问 Qwen**、**Kimi**、**MiniMax**、**Grok (xAI)**、**OpenRouter**、**豆包**、**腾讯混元 / TokenHub**、**小米 MiMo**、**智谱 GLM**、**Ollama**(本地)、**Lemonade**(本地 LLM / 图像 / TTS / ASR)以及任何兼容 OpenAI API 的服务。
+支持的服务商:**OpenAI**、**Azure OpenAI**、**Anthropic**、**Amazon Bedrock**、**Google Gemini**、**DeepSeek**、**通义千问 Qwen**、**Kimi**、**MiniMax**、**Grok (xAI)**、**OpenRouter**、**豆包**、**腾讯混元 / TokenHub**、**小米 MiMo**、**智谱 GLM**、**Ollama**(本地)、**Lemonade**(本地 LLM / 图像 / TTS / ASR)、**FunASR**(本地 ASR)以及任何兼容 OpenAI API 的服务。
Amazon Bedrock 快速示例:
@@ -164,6 +164,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 e25c05f440..da87ce1890 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
@@ -137,7 +137,7 @@ providers:
- us.anthropic.claude-opus-4-8
```
-Supported providers: **OpenAI**, **Azure OpenAI**, **Anthropic**, **Amazon Bedrock**, **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**, **Amazon Bedrock**, **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.
Amazon Bedrock quick example:
@@ -164,6 +164,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 c54a200e19..14d7e00f7c 100644
--- a/lib/i18n/locales/ar-SA.json
+++ b/lib/i18n/locales/ar-SA.json
@@ -1317,6 +1317,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 97929242fb..cb239c6c18 100644
--- a/lib/i18n/locales/en-US.json
+++ b/lib/i18n/locales/en-US.json
@@ -1317,6 +1317,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 ab2396c7fd..4c8a398f0e 100644
--- a/lib/i18n/locales/es-MX.json
+++ b/lib/i18n/locales/es-MX.json
@@ -1317,6 +1317,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 47320f18b6..6e60066118 100644
--- a/lib/i18n/locales/ja-JP.json
+++ b/lib/i18n/locales/ja-JP.json
@@ -1317,6 +1317,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 a7da323a61..df217b1032 100644
--- a/lib/i18n/locales/ko-KR.json
+++ b/lib/i18n/locales/ko-KR.json
@@ -1317,6 +1317,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 62f3be361a..599a766834 100644
--- a/lib/i18n/locales/pt-BR.json
+++ b/lib/i18n/locales/pt-BR.json
@@ -1317,6 +1317,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 3ee6928641..c8cef2e595 100644
--- a/lib/i18n/locales/ru-RU.json
+++ b/lib/i18n/locales/ru-RU.json
@@ -1317,6 +1317,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 6c0e7bb661..05334aca40 100644
--- a/lib/i18n/locales/zh-CN.json
+++ b/lib/i18n/locales/zh-CN.json
@@ -1317,6 +1317,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 bc436ac62a..4fdff85801 100644
--- a/lib/i18n/locales/zh-TW.json
+++ b/lib/i18n/locales/zh-TW.json
@@ -1250,6 +1250,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 26902d42f3..b42674e111 100644
--- a/lib/server/provider-config.ts
+++ b/lib/server/provider-config.ts
@@ -99,6 +99,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',
};
@@ -424,7 +425,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 0000000000..96b2babfa6
Binary files /dev/null and b/public/logos/funasr.png differ
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 567095f1c7..39d3c97fa0 100644
--- a/tests/server/provider-config.test.ts
+++ b/tests/server/provider-config.test.ts
@@ -36,6 +36,7 @@ const ENV_PREFIXES_TO_CLEAR = [
'TTS_MINIMAX',
'ASR_OPENAI',
'ASR_QWEN',
+ 'ASR_FUNASR',
'PDF_UNPDF',
'PDF_MINERU',
'PDF_MINERU_CLOUD',
@@ -596,6 +597,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();