Use this guide when you want Dify, n8n, webhook workers, or another workflow engine to call a private FunASR speech API. These are multipart HTTP recipes, not a compatibility guarantee for every workflow product or version.
The example server has no built-in authentication or upload limit. A client URL containing localhost does not restrict the server's listening address. Keep local checks on loopback; before sharing, configure TLS, gateway authentication, upload/time/rate limits, private health/model/schema access, and audio/transcript retention using the security and gateway guide. A placeholder API key does not authenticate the server.
Server preflight
First prepare the checkout and environment in the example README. Its fixed source revision is not a dependency/model lock or proof of a clean installation. The commands below start in that checkout's root, with its .venv already prepared. If the server is already running, skip startup and proceed to the checks; do not start a second process on the same port.
cd examples/openai_api
source ../../.venv/bin/activate
python server.py --host 127.0.0.1 --model sensevoice --device cpu --port 8000
After preparing CUDA dependencies, replace the CPU command with python server.py --host 127.0.0.1 --model sensevoice --device cuda --port 8000. For the distinct packaged funasr-server route, use Agent integration; startup defaults, aliases and response fields differ from this example.
In a second terminal on the same host, enter the same checkout's examples/openai_api directory and activate the same environment. Wait for model loading, then check the local service:
source ../../.venv/bin/activate
export FUNASR_BASE_URL="http://127.0.0.1:8000"
curl -fsS "$FUNASR_BASE_URL/health"
curl -fsS "$FUNASR_BASE_URL/v1/models"
curl -fsS "$FUNASR_BASE_URL/openapi.json"
Health and schema checks do not establish acoustic correctness. For transcription, use an existing local audio file as meeting.wav below; the README's public Chinese smoke sample is not a multilingual accuracy benchmark.
If the workflow engine runs in Docker, localhost usually means the workflow container itself. A host-loopback server is not automatically reachable from that container. Deliberately configure a private gateway/container network, then replace FUNASR_BASE_URL and the worker's FUNASR_URL with the address reachable from the workflow runtime. Use real gateway credentials when required; the local curl/Python examples below do not add authentication headers. Do not solve connectivity by exposing an unauthenticated port publicly.
Postman smoke test
Before configuring a low-code tool, you can import the Postman collection and run health, model-list, and transcription requests from a GUI. For schema-driven imports, use the OpenAPI spec. Set FUNASR_BASE_URL, choose a local audio file for the multipart file field, and keep MODEL_ALIAS=sensevoice for the first test.
For offline multi-speaker meetings, first prepare the isolated third-party MOSS service in the MOSS deployment guide, including its GPU and file-duration limits. Then set MODEL_ALIAS=moss-transcribe-diarize and keep response_format=verbose_json to preserve available native anonymous speaker segments. Do not add external VAD or spk=true; recording-local labels are not verified identities or stable IDs across recordings. Changing a client alias alone does not prepare that service.
Multipart HTTP request
Every workflow engine eventually needs to send this request shape:
- Method:
POST - URL:
http://<funasr-host>:8000/v1/audio/transcriptions - Body type:
multipart/form-data - File field:
file - Text field:
model=sensevoice - Text field:
response_format=verbose_json - Timeout: Set according to maximum audio duration, for example 300 seconds for long files.
Equivalent curl command:
curl -fsS "$FUNASR_BASE_URL/v1/audio/transcriptions" \
-F file=@meeting.wav \
-F model=sensevoice \
-F response_format=verbose_json
Map text to the transcript. response_format=verbose_json selects a format: it does not enable diarization or force timestamps. Check the client response contract before consuming the other fields:
- Example
server.py:segmentscomes only from model-providedsentence_info, otherwisesegments=[].durationis elapsed seconds aroundgenerate(), excluding initial model loading, not recording length.modelis the resolved request alias;languageechoes the submitted hint orauto, not detected language. - Packaged
funasr-server:durationis audio length in seconds, with 0 possible if fallback audio metadata cannot be read. It can return coarse text-based segments, not forced alignment. Its verbose response includestaskand segmentid/words, but no top-levelmodel; language can use backend detection. For non-native diarization models,spk=trueopts into a separate speaker pipeline, subject to its model/dependencies. The example has nospkform field.
Segment start/end use seconds, not the SDK's millisecond coordinates. A speaker field can be absent, null, numeric or a string; labels do not identify a person. Neither nonempty segments nor verbose_json guarantees accurate subtitle alignment. HTTP display text strips SenseVoice rich tags; it is not the SDK raw-tag result or a dedicated emotion/event response. SDK fields/options such as timestamp, timestamps, ctc_timestamps, use_itn, hotwords and raw arrays are not additional form fields; raw SDK timestamps are not automatically converted into HTTP segments.
Illustrative example-server response with no sentence_info; 0.42 is processing time, not audio duration:
{"text": "recognized speech", "segments": [], "language": "auto", "duration": 0.42, "model": "sensevoice"}
Illustrative packaged-server response for a 3.2-second file and a coarse fallback segment, without speaker opt-in. These examples describe schemas, not new model measurements:
{
"task": "transcribe",
"language": "en",
"duration": 3.2,
"text": "recognized speech",
"segments": [
{"id": 0, "start": 0.0, "end": 3.2, "text": "recognized speech", "words": []}
]
}
Dify custom tool or HTTP node
Use this pattern when a Dify application receives an uploaded audio file or a URL to internal audio storage.
Direct file upload path
Configure an HTTP request node or custom tool with:
- Method:
POST - URL:
http://<funasr-host>:8000/v1/audio/transcriptions - Body:
multipart/form-data - File part:
file, bound to the uploaded audio variable - Text parts:
model=sensevoice,response_format=verbose_json - Output variable: map
textas the transcript; inspectsegmentsavailability and provenance before using timestamps or speaker labels
Audio URL path
Some workflow tools pass a file URL rather than raw multipart bytes. A URL string in the multipart file field is not an audio upload. Prefer direct binary uploads or controlled storage object IDs resolved by a reviewed storage client.
The following sketch is only for an operator-approved URL in trusted storage. Destination allowlists, private-network access policy, redirect validation, download byte limits and authentication are not implemented. requests.get follows redirects and buffers the entire response; its timeout is not a byte cap or a complete end-to-end deadline. Do not pass user-supplied URLs to this helper. Before accepting them, require a reviewed download boundary enforcing all those controls, including blocking unintended private/metadata destinations and checking each redirect. Being inside a trusted network is not an SSRF defense.
For this trusted-input illustration:
- An operator supplies an approved audio URL and metadata to the worker.
- The worker downloads the file from trusted storage.
- The worker posts multipart data to FunASR.
- The worker returns the service JSON; downstream nodes check optional fields. Logs must not expose signed URLs, credentials or private transcripts.
In the same activated client environment, install the separate HTTP dependency:
python -m pip install requests
Set FUNASR_URL for the prepared service; this default is for a worker on the same host. The function definitions can be imported by your worker, but do not create an HTTP listener or implement inbound authentication/upload limits.
import requests
FUNASR_URL = "http://127.0.0.1:8000/v1/audio/transcriptions"
def transcribe_from_url(audio_url: str) -> dict:
audio_response = requests.get(audio_url, timeout=120)
audio_response.raise_for_status()
files = {"file": ("audio.wav", audio_response.content, "audio/wav")}
data = {"model": "sensevoice", "response_format": "verbose_json"}
response = requests.post(FUNASR_URL, files=files, data=data, timeout=300)
response.raise_for_status()
return response.json()
Keep this illustration restricted to approved inputs; hostname validation alone is not a complete safe-download policy.
n8n HTTP Request node
A common n8n flow is: trigger -> binary audio data -> HTTP Request -> transcript consumer.
Recommended HTTP Request settings:
- Method:
POST - URL:
http://<funasr-host>:8000/v1/audio/transcriptions - Send Body: enabled
- Body Content Type:
Form-Data/ multipart - Binary file field:
file - Additional form fields:
model=sensevoice,response_format=verbose_json - Response Format: JSON
- Timeout: Increase for long recordings.
After the request, use {{$json.text}} as the transcript. Route {{$json.segments}} onward only after checking it exists and is useful for the task; empty or coarse segments must not be treated as verified subtitle timing or diarization. Node labels and binary-property configuration vary with the installed n8n version; file is the outgoing multipart field, not necessarily the name of the incoming binary property.
n8n OpenAI Audio node
For OpenAI Audio > Transcribe node versions that send model=whisper-1, FunASR maps that compatibility alias to the model selected at server startup; it does not select a Whisper checkpoint. Set Base URL to your reachable service URL with /v1. A non-empty placeholder key is only for an unprotected local endpoint; supply real credentials for an authenticated gateway. Verify the installed node version's request behavior rather than assuming all versions match. Use this recipe for plain transcription; use the HTTP Request node for explicit response_format and any supported speaker opt-in, still subject to the response limits above.
Webhook worker pattern
Use this when the workflow engine cannot send multipart files reliably or when audio needs pre-processing. This POSIX temporary-file example uses the same requests dependency and closes the upload handle. It accepts bytes already held in memory: apply upload limits before buffering them. It is a function, not a protected webhook server.
from pathlib import Path
import tempfile
import requests
FUNASR_URL = "http://127.0.0.1:8000/v1/audio/transcriptions"
def transcribe_bytes(filename: str, payload: bytes, content_type: str = "audio/wav") -> dict:
with tempfile.NamedTemporaryFile(suffix=Path(filename).suffix or ".wav") as tmp:
tmp.write(payload)
tmp.flush()
with open(tmp.name, "rb") as audio:
response = requests.post(
FUNASR_URL,
files={"file": (filename, audio, content_type)},
data={"model": "sensevoice", "response_format": "verbose_json"},
timeout=300,
)
response.raise_for_status()
return response.json()
Audio conversion, file-size checks, request IDs, inbound/upstream authentication and retry policy are not implemented here. Enforce them at the appropriate boundary before shared use; retries can repeat expensive transcription work.
Production guardrails
- Put authentication, TLS, upload-size limits, and rate limits in front of FunASR before sharing it across teams; use the security and gateway guide for proxy and gateway patterns.
- Use
/healthfor workflow readiness checks and/v1/modelsto validate model aliases. - Log request id, audio duration, model alias, response format, device, latency, and error type.
- Set workflow timeouts according to maximum audio duration; split very long recordings before sending them through low-code tools.
- Keep private audio in trusted storage and avoid putting signed URLs, credentials, or transcripts into public logs.
- Run the same workflow with at least one public smoke sample and one realistic private sample before production use.
Troubleshooting
- Workflow can call
/healthbut transcription fails: Confirm the request ismultipart/form-dataand the binary field is namedfile. localhostconnection fails from Dify or n8n: Use the host, Compose service, or Kubernetes service reachable from the workflow runtime.- Response has no usable
segments: Check the format and deployed schema, then the model'ssentence_infoand speaker configuration;verbose_jsonalone cannot create timestamps or labels. - Requests time out: Increase HTTP timeout or split long recordings.
- First request is slow: Preload the model with
--model sensevoiceand use/healthas a readiness check. - Unknown model alias: Call
/v1/modelsand use one of the returned aliases.