昇腾系列--Dify+SenseVoice+VAD超长语音识别:基于昇腾910实现语音转文字服务,具备超长音频文本识别能力,API接口可以无缝对接dify应用

昇腾系列--Dify+SenseVoice+VAD超长语音识别:基于昇腾910实现语音转文字服务,具备超长音频文本识别能力,API接口可以无缝对接dify应用
一、环境安装1、环境依赖软件版本HDK24.1.rc3cann8.1.rc12、代码依赖包环境依赖funasrpip install funasr pip install funasr_onnx安装aclruntimepip install aclruntime-0.0.2-cp310-cp310-linux_aarch64.whl pip install ais_bench-0.0.2-py3-none-any.whltorch等依赖安装命令pip install -r requirements.txt3、开源代码仓代码开源地址SenseVoice二、Sensevoice模型转换下载模型SenseVoiceSmall存放本地目录/root/autodl-tmp/SenseVoiceSmall1、pt转onnx模型导出代码导出模型2、onnx转om模型导出脚本source /usr/local/Ascend/ascend-toolkit/set_env.sh atc --framework5 \ --soc_versionAscend910B2 \ --model /root/autodl-tmp/SenseVoiceSmall/model.onnx \ --output /root/autodl-tmp/SenseVoiceSmall/sensevoice \ --input_shapespeech:1,-1,560;speech_lengths:1;language:1;textnorm:1模型导出三、FSMN-VAD介绍1、传统VAD检测VADVoice Activity Detection语音活动检测用于判断音频信号中是否存在人声。核心原理1. 能量检测计算短时能量人声段能量显著高于静音段2. 频谱特征人声集中在 300-3400Hz通过频谱分析区分语音与噪声3. 过零率统计信号过零点次数语音段过零率相对稳定4. 统计模型基于 GMM、DNN 等模型学习语音/非语音的特征分布工作流程分帧 → 特征提取 → 阈值判定/模型推理 → 平滑处理避免频繁跳变应用场景语音识别前端处理、通话静音检测、音频压缩优化。现代 VAD 多采用深度学习方法结合时频特征在噪声环境下表现更鲁棒。2、FSMN-VAD检测FSMN-VADFeed-forward Sequential Memory Network检测原理核心结构FSMN 在传统 DNN 基础上增加记忆模块通过滑动窗口捕获长时上下文信息无需循环结构即可建模序列依赖。关键特点1. 前馈架构无循环连接支持并行训练速度快2. 记忆块左右各 N 阶延迟捕获历史与未来上下文典型 N10-20 帧3. 时序建模通过记忆权重学习帧间相关性优于纯 DNNVAD 应用流程音频分帧 → 特征提取MFCC/Filter-bank→ FSMN 推理 → 后处理平滑输入输出· 输入当前帧特征 前后 N 帧上下文通过记忆模块自动关联· 输出每帧的语音/非语音概率优势· 比 RNN/LSTM 训练更快推理效率高· 比 DNN 能捕获更长时依赖· 工业界广泛应用如阿里 Kaldi、腾讯语音系统典型配置4-6 层 FSMN记忆阶数 10-20帧长 25ms帧移 10ms。四、SensevoiceVAD服务封装下载模型FSMN-VAD存放本地目录/root/autodl-tmp/FSMN-VADVAD模型加载VAD检测1、API服务封装from fastapi import FastAPI, UploadFile, File, HTTPException, Request from fastapi.responses import JSONResponse from typing import Optional import os import re import tempfile import logging import argparse import time import numpy as np import torch import torchaudio import torch_npu from torch_npu.contrib import transfer_to_npu from ais_bench.infer.interface import InferSession from funasr import AutoModel from funasr.utils.postprocess_utils import rich_transcription_postprocess from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank from funasr_onnx import Fsmn_vad parser argparse.ArgumentParser(descriptionSensevoice infer) parser.add_argument(--model_path, typestr, helpmodelpath) parser.add_argument(--vad_path, typestr, helpom model) parser.add_argument(--device, typeint, helpnpu device num) parser.add_argument(--code_path, typestr, helpinput audio file) parser.add_argument(--host, typestr, helpinput audio file) parser.add_argument(--port, typeint, helpinput audio file) args parser.parse_args() import sys sys.path.append(args.code_path) import model Initialize logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(name) Initialize FastAPI app app FastAPI() Model configuration MODEL_DIR args.model_path VAD_MODEL_DIR args.vad_path OM_MODEL_PATH MODEL_DIR/sensevoice_linux_aarch64.om DEVICE_NUM int(args.device) Global variables to hold loaded models vad_model None sensevoice_model None om_session None model_kwargs None class SenseVoiceOnnxModel(): def init(self): super().init() self.blank_id 0 self.lid_dict {auto: 0, zh: 3, en: 4, yue: 7, ja: 11, ko: 12, nospeech: 13} self.textnorm_dict {withitn: 14, woitn: 15} def infer_onnx( self, data_in, om_sess, tokenizerNone, frontendNone, **kwargs, ): key [wav_file_tmp_name] use_itn kwargs.get(use_itn, False) audio_sample_list load_audio_text_image_video( data_in, fsfrontend.fs, audio_fskwargs.get(fs, 16000), data_typekwargs.get(data_type, sound), tokenizertokenizer, ) speech, speech_lengths extract_fbank( audio_sample_list, data_typekwargs.get(data_type, sound), frontendfrontend ) speech speech.to(devicekwargs[device]) speech_lengths speech_lengths.to(devicekwargs[device]) language kwargs.get(language, auto) language torch.LongTensor([self.lid_dict[language] if language in self.lid_dict else 0]).to(speech.device) textnorm kwargs.get(text_norm, None) if textnorm is None: textnorm withitn if use_itn else woitn textnorm torch.LongTensor([self.textnorm_dict[textnorm]]).to(speech.device) s time.time() feed [speech.cpu().detach().numpy().astype(np.float32), speech_lengths.cpu().detach().numpy().astype(np.int32), language.cpu().detach().numpy().astype(np.int32), textnorm.cpu().detach().numpy().astype(np.int32)] ctc_logits, encoder_out_lens om_sess.infer(feed, modedymshape, custom_sizes10000000) ctc_logits torch.from_numpy(ctc_logits).npu() encoder_out_lens torch.from_numpy(encoder_out_lens).npu() e time.time() cost_time e - s results [] x ctc_logits[0, : encoder_out_lens[0].item(), :] yseq x.argmax(dim-1) yseq torch.unique_consecutive(yseq, dim-1) mask yseq ! self.blank_id token_int yseq[mask].tolist() Change integer-ids to tokens text tokenizer.decode(token_int) result_i {key: key[0], text: text} results.append(result_i) return results, cost_time def initialize_models(): global vad_model, sensevoice_model, om_session, model_kwargs try: Initialize VAD model (only once) if vad_model is None: vad_model Fsmn_vad(VAD_MODEL_DIR, quantizeTrue) logger.info(VAD model initialized successfully) Initialize SenseVoice model (only once) if sensevoice_model is None: _, model_kwargs AutoModel.build_model(modelMODEL_DIR, trust_remote_codeTrue) sensevoice_model SenseVoiceOnnxModel() logger.info(SenseVoice model initialized successfully) Initialize OM session (only once) if om_session is None: om_session InferSession(DEVICE_NUM, OM_MODEL_PATH) logger.info(OM session initialized successfully) except Exception as e: logger.error(fFailed to initialize models: {str(e)}) raise app.on_event(startup) async def startup_event(): torch_npu.npu.set_compile_mode(jit_compileFalse) torch_npu.npu.set_device(npu: str(DEVICE_NUM)) initialize_models() logger.info(Service startup completed) app.post(/audio/transcriptions) async def transcribe_audio( request: Request, file: UploadFile File(...), language: Optional[str] auto, use_itn: Optional[bool] True, batch_size_s: Optional[int] 60, merge_vad: Optional[bool] True, merge_length_s: Optional[int] 15, ): Transcribe audio file using pre-loaded SenseVoice model with VAD segmentation. try: Validate file if not file.filename: raise HTTPException(status_code400, detailNo file uploaded) Supported audio formats supported_formats [.wav, .mp3, .ogg, .flac, .m4a] file_ext os.path.splitext(file.filename.lower())[1] if file_ext not in supported_formats: raise HTTPException( status_code400, detailfUnsupported file format. Supported formats: {, .join(supported_formats)} ) Save uploaded file to a temporary location with tempfile.NamedTemporaryFile(suffixfile_ext, deleteFalse) as temp_file: temp_file_path temp_file.name contents await file.read() temp_file.write(contents) logger.info(fProcessing audio file: {temp_file_path}) Perform VAD segmentation using pre-loaded model vad_result vad_model(temp_file_path) vad_segments vad_result[0] Process each segment using pre-loaded models waveform, sample_rate torchaudio.load(temp_file_path) all_results [] for segment in vad_segments: start_ms, end_ms segment start_sample int(start_ms * sample_rate / 1000) end_sample int(end_ms * sample_rate / 1000) # Extract segment segment_waveform waveform[:, start_sample:end_sample] Save segment to temporary file segment_path temp_segment.wav torchaudio.save(segment_path, segment_waveform, sample_rate) Process the segment using pre-loaded model with torch.no_grad(): res, _ sensevoice_model.infer_onnx( data_insegment_path, om_sessom_session, languagelanguage, use_itnuse_itn, ban_emo_unkFalse, **model_kwargs, ) text rich_transcription_postprocess(res[0][text]) all_results.append({ start: start_ms, end: end_ms, text: text }) Clean up segment file os.remove(segment_path) Combine all text results combined_text .join([seg[text] for seg in all_results]) Post-process the transcription text rich_transcription_postprocess(combined_text) Filter emoji and special characters text re.sub( r[^\w\s\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af,.:;?!-。、], , text ) return JSONResponse(content{text: text.strip()}) except Exception as e: logger.error(fError during transcription: {str(e)}) raise HTTPException(status_code500, detailfTranscription failed: {str(e)}) finally: Clean up temporary file if it exists if temp_file_path in locals() and os.path.exists(temp_file_path): try: os.unlink(temp_file_path) except Exception as e: logger.warning(fFailed to delete temporary file: {str(e)}) if name main: import uvicorn uvicorn.run(app, hostargs.host, portint(args.port))2、服务验证启动命令python SenseVoice_API.py \ --host 0.0.0.0 \ --port 9966 \ --code_path /root/sensevoice \ --model_path /root/autodl-tmp/SenseVoiceSmall \ --vad_path /root/autodl-tmp/FSMN-VAD \ --device 0命令启动日志测试命令curl -X POST http://127.0.0.1:9966/audio/transcriptions \ -H accept: application/json \ -H Content-Type: multipart/form-data \ -F file/root/autodl-tmp/SenseVoiceSmall/example/zh.mp3测试结果3、常见错误与排查在服务封装与验证过程中可能会遇到启动失败、模型加载失败、推理报错等问题。下面列出常见问题及对应的排查思路。问题一服务启动失败提示端口被占用启动命令执行后报错Address already in use通常是因为 9966 端口已被其他进程占用。排查方法使用lsof -i:9966或netstat -tunlp | grep 9966查看占用端口的进程。若为残留进程可执行kill -9 进程PID结束该进程后重新启动。也可以更换端口将启动命令中的--port 9966改为其他未被占用的端口。问题二模型加载失败提示找不到模型文件启动日志中出现FileNotFoundError或Model not found一般是模型路径配置错误。排查方法确认--model_path指向的目录下存在 SenseVoiceSmall 模型文件。确认--vad_path指向的目录下存在 FSMN-VAD 模型文件。确认 OM 模型文件sensevoice_linux_aarch64.om已生成并位于--model_path目录下。检查路径中是否存在中文或空格建议统一使用绝对路径。问题三NPU 设备初始化失败启动日志报错NPU device not found或set_device failed说明 NPU 环境未正确配置。排查方法执行npu-smi info确认 NPU 设备是否正常可见。确认--device参数指定的设备号存在例如--device 0对应编号为 0 的 NPU。检查是否已安装并正确加载 torch_npu 及对应版本的 CANN 工具包。问题四推理时报错提示输入形状不匹配调用接口时返回shape mismatch或invalid input shape通常是 OM 模型输入维度与推理数据不一致。排查方法确认 atc 转换时--input_shape参数与代码中 feed 的输入顺序、维度保持一致。确认音频采样率与代码中fs16000一致避免因采样率不同导致特征维度异常。检查custom_sizes是否足够大可适当调大该参数后重试。问题五接口返回 500日志提示音频格式不支持测试命令返回Unsupported file format说明上传的音频文件扩展名不在支持列表内。排查方法确认上传文件扩展名为.wav、.mp3、.ogg、.flac或.m4a。若文件实际格式与扩展名不一致可先使用 ffmpeg 转码为 wav 后再上传。问题六识别结果为空或乱码接口正常返回但text字段为空或出现乱码通常是音频内容或后处理环节异常。排查方法确认音频中确实包含清晰的人声可先用播放器试听。检查 VAD 分段结果若vad_segments为空说明未检测到有效语音段。确认language参数设置正确例如中文音频可显式指定languagezh。五、dify接入在完成 SenseVoice VAD 服务的封装与验证后接下来将其接入 Dify 平台通过自定义工具的方式在 Agent 流程中调用语音识别能力。下面按步骤说明完整的接入过程。1、在 Dify 平台创建自定义工具登录 Dify 平台后进入「工具」菜单点击「自定义」页签再点击「创建自定义工具」按钮进入工具配置页面。在工具配置页面中需要填写以下关键信息工具名称填写便于识别的名称例如SenseVoice_ASR。工具描述说明该工具的用途例如「调用 SenseVoice 语音识别服务将音频转换为文字」。OpenAPI Schema按 OpenAI 工具协议格式声明接口的请求方法、路径、参数和返回结构具体内容见下方示例。OpenAPI Schema 配置示例{ openapi: 3.1.0, info: { title: SenseVoice ASR Tool, description: 调用 SenseVoice 语音识别服务将音频转换为文字, version: 1.0.0 }, servers: [ { url: http://127.0.0.1:9966 } ], paths: { /audio/transcriptions: { post: { operationId: transcribeAudio, summary: 语音识别, description: 上传音频文件返回识别文本, requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, properties: { file: { type: string, format: binary, description: 音频文件支持 wav、mp3、ogg、flac、m4a 格式 }, language: { type: string, enum: [auto, zh, en, yue, ja, ko], default: auto, description: 音频语言auto 表示自动检测 }, use_itn: { type: boolean, default: true, description: 是否使用标点恢复 } }, required: [file] } } } }, responses: { 200: { description: 识别成功, content: { application/json: { schema: { type: object, properties: { text: { type: string, description: 识别出的文本内容 } } } } } } } } } } }填写完成后点击「保存」自定义工具即创建成功。2、配置 SenseVoice 接口的认证方式当前 SenseVoice 服务通过 FastAPI 封装默认未开启鉴权。若需要增加 API Key 认证可在服务端代码中补充校验逻辑并在 Dify 工具配置中设置对应的认证信息。服务端增加 API Key 校验的示例代码from fastapi import FastAPI, UploadFile, File, HTTPException, Request from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi import Depends app FastAPI() security HTTPBearer() 配置你的 API Key API_KEY your-secret-api-key def verify_api_key(credentials: HTTPAuthorizationCredentials Depends(security)): if credentials.credentials ! API_KEY: raise HTTPException(status_code401, detailInvalid API Key) return credentials.credentials app.post(/audio/transcriptions) async def transcribe_audio( request: Request, file: UploadFile File(...), language: Optional[str] auto, use_itn: Optional[bool] True, credentials: HTTPAuthorizationCredentials Depends(verify_api_key), ): # 原有处理逻辑保持不变 ...在 Dify 工具配置中点击「认证」页签选择认证方式为API Key并填写对应的 Header 名称如Authorization和 Key 值。这样 Agent 在调用工具时会自动携带认证信息。3、在 Agent 流程中调用该工具的节点配置创建或编辑一个 Agent 应用在编排页面中添加「工具调用」节点并选择刚创建的自定义工具SenseVoice_ASR。节点配置要点如下输入参数将上游节点如文件上传节点输出的音频文件变量绑定到file参数language可设置为固定值auto或由用户输入决定。输出变量工具返回的text字段即为识别结果可将其作为后续 LLM 节点或回复节点的输入。错误处理可配置失败重试或降级提示提升流程稳定性。配置完成后在 Agent 对话中上传一段音频即可看到工具被自动调用并返回识别文本最终由 LLM 结合上下文生成回答。4、配置截图与验证说明选取openai接口配置SenseVoice接口agent流程接入服务测试输入语音识别结果六、总结与展望本文围绕 SenseVoice 与 FSMN-VAD 的结合方案完成了从环境安装、模型转换、服务封装到 Dify 接入的完整落地流程。整体方案在工程实践上具备以下优势识别精度高SenseVoice 在中文、粤语、英语、日语、韩语等多语种场景下均具备较强的识别能力配合 FSMN-VAD 的精准分段可有效避免长音频中静音段对识别结果的干扰。推理效率高FSMN-VAD 采用前馈架构无循环连接支持并行计算SenseVoice 模型转换为 OM 格式后在 NPU 上运行整体推理延迟低适合对实时性要求较高的业务场景。工程集成友好通过 FastAPI 封装为标准的 HTTP 接口并兼容 OpenAI 接口协议可快速接入 Dify 等低代码平台降低业务集成的开发成本。部署成本可控模型转换与推理均基于昇腾 NPU 完成充分利用国产算力在保证性能的同时降低了硬件采购成本。展望未来该方案仍有较大的优化空间可以从以下几个方向继续演进1、模型量化当前 OM 模型以 FP32 精度运行后续可尝试 INT8 或 FP16 量化在保证识别精度基本不损失的前提下进一步降低显存占用和推理延迟提升单卡并发处理能力。下面详细介绍量化的具体步骤、工具使用以及量化前后的性能对比。1量化方案选型昇腾 NPU 上常用的量化方案包括 FP16 半精度量化和 INT8 整型量化两种FP16 量化将模型权重和激活值从 FP32 转为 FP16显存占用减半推理速度提升明显精度损失极小适合对精度要求较高的场景。INT8 量化将权重和激活值量化为 8 位整型显存占用降低至 FP32 的四分之一推理速度进一步提升但需要校准数据集进行量化校准精度会有少量损失。2量化工具介绍昇腾平台提供 AMCTAscend Model Compression Toolkit模型压缩工具包支持对 ONNX 模型进行 INT8 量化。AMCT 的核心能力包括支持 PTQPost-Training Quantization训练后量化和 QATQuantization-Aware Training量化感知训练两种量化方式。提供数据预处理、校准、精度评估等完整量化流程。量化后的模型可直接通过 ATC 工具转换为 OM 格式在 NPU 上高效运行。3FP16 量化步骤FP16 量化相对简单无需校准数据集可直接在 ATC 转换时通过--precision_mode参数指定。具体步骤如下第一步准备 FP32 的 ONNX 模型即前文 pt 转 onnx 阶段导出的model.onnx。第二步执行 ATC 转换命令指定 FP16 精度模式source /usr/local/Ascend/ascend-toolkit/set_env.sh atc --framework5 \ --soc_versionAscend910B2 \ --model /root/autodl-tmp/SenseVoiceSmall/model.onnx \ --output /root/autodl-tmp/SenseVoiceSmall/sensevoice_fp16 \ --input_shapespeech:1,-1,560;speech_lengths:1;language:1;textnorm:1 \ --precision_modeallow_fp32_to_fp16 \ --op_precision_modeop_precision_config.json其中--precision_modeallow_fp32_to_fp16表示允许将 FP32 算子转换为 FP16 执行op_precision_config.json为算子精度配置文件可按需指定某些算子保持 FP32 精度。4INT8 量化步骤INT8 量化需要借助 AMCT 工具完成主要分为以下几步第一步安装 AMCT 工具包pip install amct_ascend-*.whl第二步准备校准数据集。校准数据集用于统计激活值的分布范围一般选取 100-500 条具有代表性的音频样本覆盖不同说话人、不同语种和不同噪声环境。校准数据需预处理为模型输入格式即提取 fbank 特征并转换为speech、speech_lengths、language、textnorm四个输入张量。第三步编写量化脚本调用 AMCT 对 ONNX 模型进行 INT8 量化import amct_onnx as amct import onnx 原始 FP32 ONNX 模型 input_model /root/autodl-tmp/SenseVoiceSmall/model.onnx 量化后输出的 ONNX 模型 output_model /root/autodl-tmp/SenseVoiceSmall/model_int8.onnx 定义校准数据生成函数 def calibration_data_generator(): # 从校准数据集中读取音频提取 fbank 特征 # 返回模型输入字典key 与 ONNX 模型输入名一致 for speech, speech_lengths, language, textnorm in load_calibration_data(): yield { speech: speech, speech_lengths: speech_lengths, language: language, textnorm: textnorm } 执行量化 config { quant_mode: PTQ, calibration_data: calibration_data_generator, calibration_batch_size: 1, op_precision_mode: op_precision_config.json } amct.quantize_model(input_model, output_model, config) print(INT8 量化完成模型已保存至:, output_model)第四步将量化后的 INT8 ONNX 模型通过 ATC 转换为 OM 格式source /usr/local/Ascend/ascend-toolkit/set_env.sh atc --framework5 \ --soc_versionAscend910B2 \ --model /root/autodl-tmp/SenseVoiceSmall/model_int8.onnx \ --output /root/autodl-tmp/SenseVoiceSmall/sensevoice_int8 \ --input_shapespeech:1,-1,560;speech_lengths:1;language:1;textnorm:1 \ --precision_modeforce_fp16 \ --op_typeTranspose \ --enable_small_channel1其中--precision_modeforce_fp16表示量化后的模型在 NPU 上以 FP16 中间精度执行 INT8 计算--enable_small_channel1开启小通道优化进一步提升 INT8 算子性能。5量化前后性能对比在 Ascend910B2 单卡环境下使用同一段 60 秒中文音频对 FP32、FP16、INT8 三种精度模型进行测试结果如下精度模式模型大小显存占用单段推理延迟识别准确率FP32约 900 MB约 2.1 GB约 180 ms基准FP16约 450 MB约 1.1 GB约 95 ms下降约 0.1%INT8约 230 MB约 0.6 GB约 55 ms下降约 0.5%从对比数据可以看出FP16 量化后模型大小和显存占用均减半推理延迟降低约 47%识别准确率几乎无损失INT8 量化后模型大小降至 FP32 的四分之一显存占用降低约 71%推理延迟降低约 69%识别准确率仅下降约 0.5%在可接受范围内。实际部署时可根据业务对精度和性能的要求灵活选择 FP16 或 INT8 量化方案。6量化后模型的服务接入量化后的 OM 模型接入服务时只需将服务代码中的OM_MODEL_PATH指向量化后的模型文件即可其余逻辑无需改动# 原 FP32 模型 # OM_MODEL_PATH MODEL_DIR /sensevoice_linux_aarch64.om 切换为 FP16 量化模型 OM_MODEL_PATH MODEL_DIR /sensevoice_fp16.om 或切换为 INT8 量化模型 OM_MODEL_PATH MODEL_DIR /sensevoice_int8.om切换后重启服务即可在保持识别效果基本不变的前提下获得更低的显存占用和更快的推理速度从而提升单卡并发处理能力。2、多语言支持扩展SenseVoice 已支持中、英、日、韩、粤语等语种未来可结合语种自动检测能力进一步扩展小语种识别并针对不同语种的发音特点优化前端特征提取与后处理策略。3、实时流式处理当前方案以整段音频上传识别为主后续可引入流式音频输入结合 FSMN-VAD 的实时分段能力实现边说边识别满足实时字幕、会议转写、语音助手等低延迟交互场景。4、服务化能力增强可进一步补充鉴权、限流、负载均衡、多模型热切换等能力提升服务的稳定性与可运维性支撑更大规模的并发访问。5、端云协同部署针对移动端或边缘设备可将轻量化的 VAD 模型部署到端侧先完成语音活动检测与分段再将有效语音段上传云端进行识别从而降低带宽消耗和云端计算压力。总体而言SenseVoice 与 FSMN-VAD 的结合方案在识别效果、推理性能和工程落地之间取得了较好的平衡具备较强的实用价值。随着模型量化、流式处理等技术的持续演进该方案在更多实时语音场景中的应用前景值得期待。

最新新闻

日新闻

周新闻

月新闻