From 1c9b4b9958745b8e643ecbb46e937937e0edeeba Mon Sep 17 00:00:00 2001 From: LauraGPT <18321252+LauraGPT@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:20:13 +0000 Subject: [PATCH] feat(server): support n8n OpenAI audio transcription Signed-off-by: LauraGPT <18321252+LauraGPT@users.noreply.github.com> --- examples/openai_api/WORKFLOWS.md | 10 ++++++++++ examples/openai_api/WORKFLOWS_zh.md | 8 ++++++++ funasr/bin/_server_app.py | 12 ++++++++++++ tests/test_server_app_openai_segments.py | 23 +++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/examples/openai_api/WORKFLOWS.md b/examples/openai_api/WORKFLOWS.md index 43a6d603b..3cdab6ba6 100644 --- a/examples/openai_api/WORKFLOWS.md +++ b/examples/openai_api/WORKFLOWS.md @@ -122,6 +122,16 @@ Recommended HTTP Request settings: After the request, use `{{$json.text}}` as the transcript. If `verbose_json` is enabled, route `{{$json.segments}}` to subtitle, speaker, or QA steps. +### n8n OpenAI Audio node + +The built-in OpenAI node can also run its Audio > Transcribe operation against +FunASR. In OpenAI credentials, set Base URL to +`http://:8000/v1` and provide any non-empty API key. The node +always sends `model=whisper-1`; FunASR maps that compatibility alias to the +model selected when the server starts. This path is transcription-only. Use the +HTTP Request node above when you need `verbose_json`, segments, or speaker +labels. + ## Webhook worker pattern Use this when the workflow engine cannot send multipart files reliably or when audio needs pre-processing. diff --git a/examples/openai_api/WORKFLOWS_zh.md b/examples/openai_api/WORKFLOWS_zh.md index 21979407c..7e05b7c07 100644 --- a/examples/openai_api/WORKFLOWS_zh.md +++ b/examples/openai_api/WORKFLOWS_zh.md @@ -121,6 +121,14 @@ def transcribe_from_url(audio_url: str) -> dict: 请求之后,使用 `{{$json.text}}` 作为转写文本。如果启用了 `verbose_json`,可以把 `{{$json.segments}}` 传给字幕、说话人分析或质检节点。 +### n8n OpenAI Audio 节点 + +也可以让内置 OpenAI 节点的 Audio > Transcribe 操作调用 FunASR。在 OpenAI +凭据中将 Base URL 设为 `http://:8000/v1`,并填写任意非空 API +key。该节点固定发送 `model=whisper-1`,FunASR 会将这个兼容别名映射到服务 +启动时选择的模型。此路径只返回转写文本;需要 `verbose_json`、分段或说话人 +标签时,仍使用上面的 HTTP Request 节点。 + ## Webhook worker 模式 当工作流引擎不能稳定发送 multipart 文件,或音频需要预处理时,可以把转写封装成一个内部 webhook worker。 diff --git a/funasr/bin/_server_app.py b/funasr/bin/_server_app.py index 95db8a63f..588f1fbb1 100644 --- a/funasr/bin/_server_app.py +++ b/funasr/bin/_server_app.py @@ -35,6 +35,7 @@ PACKAGE_VERSION = (Path(__file__).resolve().parents[1] / "version.txt").read_tex _LANGUAGE_TAG_RE = re.compile(r"<\|(zh|en|yue|ja|ko)\|>") MOSS_MODEL_REVISION = "e8681d68e7042738ffca8ac8212bc8fcb1131ab8" NATIVE_DIARIZATION_MODELS = {"moss-transcribe-diarize"} +N8N_OPENAI_MODEL_ALIAS = "whisper-1" def extract_language_from_asr_text(text): @@ -55,6 +56,13 @@ def resolve_transcription_language(requested_language, result): return "unknown" +def resolve_openai_transcription_model(requested_model, default_model): + """Map n8n's fixed OpenAI transcription model to the server default.""" + if requested_model == N8N_OPENAI_MODEL_ALIAS: + return default_model + return requested_model + + def _split_text_for_openai_segments(text: str, max_chars: int = 80): """Split unsegmented ASR text into readable OpenAI-compatible cues.""" text = text.strip() @@ -233,6 +241,7 @@ def create_app( app.state.fallback_models = {} app.state.model_path = model_path app.state.hub = hub + app.state.openai_transcription_model = "custom" if model_path else preload_model normalized_origins = [] for origin in cors_origins or []: @@ -480,6 +489,9 @@ def create_app( ): content = await file.read() t0 = time.perf_counter() + model = resolve_openai_transcription_model( + model, app.state.openai_transcription_model + ) if model == "fun-asr-nano": _load_vllm_engine() diff --git a/tests/test_server_app_openai_segments.py b/tests/test_server_app_openai_segments.py index f97dedadf..06a4951f8 100644 --- a/tests/test_server_app_openai_segments.py +++ b/tests/test_server_app_openai_segments.py @@ -237,6 +237,29 @@ def test_verbose_json_reports_sensevoice_detected_language(monkeypatch): ] +def test_n8n_whisper_model_alias_uses_preloaded_model(monkeypatch): + module = load_server_app(monkeypatch) + DummyAutoModel = install_dummy_funasr(monkeypatch) + monkeypatch.setattr(module.sf, "info", lambda path: types.SimpleNamespace(duration=1.25)) + app = module.create_app(device="cpu", preload_model="sensevoice") + transcribe = app.routes[("POST", "/v1/audio/transcriptions")] + + response = asyncio.run( + transcribe( + file=DummyUpload(), + model="whisper-1", + language=None, + response_format="json", + spk=False, + ) + ) + + assert response == {"text": "transcript"} + assert [instance["model"] for instance in DummyAutoModel.instances] == [ + "iic/SenseVoiceSmall" + ] + + def test_openai_verbose_json_preserves_speaker_labels(monkeypatch): module = load_server_app(monkeypatch)