-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[fix] 修复了sensevoice STT重复下载模型文件和缺失依赖项的问题 #9897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,78 +1,173 @@ | ||||||||||||||||||||||||
| """Author: diudiu62 | ||||||||||||||||||||||||
| Date: 2025-02-24 18:04:18 | ||||||||||||||||||||||||
| LastEditTime: 2025-02-25 14:06:30 | ||||||||||||||||||||||||
| LastEditTime: 2026-08-31 | ||||||||||||||||||||||||
| LastEdit / Blame: xiewoc | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||
| from typing import cast | ||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||
| from typing import Optional, TYPE_CHECKING | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from funasr_onnx import SenseVoiceSmall | ||||||||||||||||||||||||
| from funasr_onnx.utils.postprocess_utils import rich_transcription_postprocess | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from astrbot.core import logger | ||||||||||||||||||||||||
| from astrbot.core.utils.pip_installer import PipInstaller | ||||||||||||||||||||||||
| from astrbot.core.utils.media_utils import MediaResolver | ||||||||||||||||||||||||
| from astrbot.core.utils.astrbot_path import get_astrbot_data_path | ||||||||||||||||||||||||
| from astrbot.core import logger | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from ..entities import ProviderType | ||||||||||||||||||||||||
| from ..provider import STTProvider | ||||||||||||||||||||||||
| from ..register import register_provider_adapter | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 仅在类型检查时导入,避免运行时循环依赖或导入失败 | ||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||
| from funasr_onnx import SenseVoiceSmall | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _REQUIRED_MODULES = [ | ||||||||||||||||||||||||
| "funasr", "funasr_onnx", "torch", # "torchaudio", <- we don't need this, use FFmpeg instead | ||||||||||||||||||||||||
| "onnxruntime", "modelscope", "onnxscript" | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 模块级缓存,避免重复安装/导入 | ||||||||||||||||||||||||
| _sense_voice_cls = None | ||||||||||||||||||||||||
| _postprocess_fn = None | ||||||||||||||||||||||||
| _snapshot_download_fn = None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _check_ffmpeg() -> None: | ||||||||||||||||||||||||
| """检测系统是否安装了 FFmpeg,未安装则记录错误并抛出异常阻断流程。""" | ||||||||||||||||||||||||
| if shutil.which("ffmpeg") is None: | ||||||||||||||||||||||||
| msg = ( | ||||||||||||||||||||||||
| "未检测到 FFmpeg!SenseVoice STT 依赖 FFmpeg 进行音频转码。" | ||||||||||||||||||||||||
| "请安装 FFmpeg 并将其添加到系统 PATH 环境变量中。" | ||||||||||||||||||||||||
| "参考: https://ffmpeg.org/download.html" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| logger.error(msg) | ||||||||||||||||||||||||
| raise RuntimeError(msg) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async def _install_dependencies(): | ||||||||||||||||||||||||
| """异步安装依赖库""" | ||||||||||||||||||||||||
| pip = PipInstaller(pip_install_arg="") | ||||||||||||||||||||||||
| for item in _REQUIRED_MODULES: | ||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| await pip.install(item) | ||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||
| logger.error(f"安装依赖 {item} 失败: {e}") | ||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _load_sense_voice_modules(): | ||||||||||||||||||||||||
| """延迟加载 SenseVoice 相关模块,支持自动安装重试""" | ||||||||||||||||||||||||
| global _sense_voice_cls, _postprocess_fn, _snapshot_download_fn | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if _sense_voice_cls is not None: | ||||||||||||||||||||||||
| return _sense_voice_cls, _postprocess_fn, _snapshot_download_fn | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| from modelscope import snapshot_download | ||||||||||||||||||||||||
| from funasr_onnx import SenseVoiceSmall | ||||||||||||||||||||||||
| from funasr_onnx.utils.postprocess_utils import rich_transcription_postprocess | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _sense_voice_cls = SenseVoiceSmall | ||||||||||||||||||||||||
| _postprocess_fn = rich_transcription_postprocess | ||||||||||||||||||||||||
| _snapshot_download_fn = snapshot_download | ||||||||||||||||||||||||
| return _sense_voice_cls, _postprocess_fn, _snapshot_download_fn | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||
| logger.info("SenseVoice 依赖未安装,正在尝试自动安装...") | ||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| # 注意:在已有事件循环中应使用 run_coroutine_threadsafe 或 nest_asyncio | ||||||||||||||||||||||||
| # 此处保留原逻辑但增加提示,实际部署建议改为插件初始化钩子 | ||||||||||||||||||||||||
| loop = asyncio.get_event_loop() | ||||||||||||||||||||||||
| if loop.is_running(): | ||||||||||||||||||||||||
| logger.warning("检测到运行中的事件循环,自动安装可能失败。建议手动安装依赖。") | ||||||||||||||||||||||||
| asyncio.run(_install_dependencies()) | ||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||
| logger.error(f"自动安装依赖失败: {e}") | ||||||||||||||||||||||||
| raise ImportError( | ||||||||||||||||||||||||
| "SenseVoice 依赖安装失败,请手动执行: pip install " + " ".join(_REQUIRED_MODULES) | ||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 重试导入 | ||||||||||||||||||||||||
| from modelscope import snapshot_download | ||||||||||||||||||||||||
| from funasr_onnx import SenseVoiceSmall | ||||||||||||||||||||||||
| from funasr_onnx.utils.postprocess_utils import rich_transcription_postprocess | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _sense_voice_cls = SenseVoiceSmall | ||||||||||||||||||||||||
| _postprocess_fn = rich_transcription_postprocess | ||||||||||||||||||||||||
| _snapshot_download_fn = snapshot_download | ||||||||||||||||||||||||
| return _sense_voice_cls, _postprocess_fn, _snapshot_download_fn | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @register_provider_adapter( | ||||||||||||||||||||||||
| "sensevoice_stt_selfhost", | ||||||||||||||||||||||||
| "SenseVoice 自托管语音识别 模型部署", | ||||||||||||||||||||||||
| "SenseVoice 自托管语音识别模型部署", | ||||||||||||||||||||||||
| provider_type=ProviderType.SPEECH_TO_TEXT, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| class ProviderSenseVoiceSTTSelfHost(STTProvider): | ||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||
| provider_config: dict, | ||||||||||||||||||||||||
| provider_settings: dict, | ||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||
| def __init__(self, provider_config: dict, provider_settings: dict) -> None: | ||||||||||||||||||||||||
| super().__init__(provider_config, provider_settings) | ||||||||||||||||||||||||
| self.set_model(provider_config["stt_model"]) | ||||||||||||||||||||||||
| self.model = None | ||||||||||||||||||||||||
| self.is_emotion = provider_config.get("is_emotion", False) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self.model: Optional["SenseVoiceSmall"] = None | ||||||||||||||||||||||||
| self.is_emotion: bool = provider_config.get("is_emotion", False) | ||||||||||||||||||||||||
| self.model_path: str = os.path.join(get_astrbot_data_path(), "SenseVoiceSmall") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async def initialize(self) -> None: | ||||||||||||||||||||||||
| logger.info("下载或者加载 SenseVoice 模型中,这可能需要一些时间 ...") | ||||||||||||||||||||||||
| # ✅ 优先检测 FFmpeg,缺失时快速失败,避免浪费模型下载/加载时间 | ||||||||||||||||||||||||
| _check_ffmpeg() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| logger.info("正在下载或加载 SenseVoice 模型,首次可能需要较长时间...") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| SenseVoiceSmall, _, snapshot_download = _load_sense_voice_modules() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 模型下载(同步操作放入线程池) | ||||||||||||||||||||||||
| if not os.path.exists(os.path.join(self.model_path, "configuration.json")): | ||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||
| await loop.run_in_executor( | ||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||
| lambda: snapshot_download("iic/SenseVoiceSmall", local_dir=self.model_path), | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The configured Triggers: When a provider configuration specifies an Suggested fix: Use |
||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+126
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The existence of Triggers: When the model directory contains Suggested fix: Validate the complete expected model file set, or let
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 将模型加载放到线程池中执行 | ||||||||||||||||||||||||
| # 模型加载(CPU/GPU 密集型操作放入线程池) | ||||||||||||||||||||||||
| self.model = await asyncio.get_running_loop().run_in_executor( | ||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||
| lambda: SenseVoiceSmall(self.model_name, quantize=True, batch_size=16), | ||||||||||||||||||||||||
| lambda: SenseVoiceSmall(model_dir=self.model_path, quantize=True, batch_size=16), | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| logger.info("SenseVoice 模型加载完成。") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async def get_text(self, audio_url: str) -> str: | ||||||||||||||||||||||||
| if self.model is None: | ||||||||||||||||||||||||
| raise RuntimeError("SenseVoice 模型未初始化,请先调用 initialize()") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 局部变量绑定,确保类型收窄且避免 lambda 中的属性访问问题 | ||||||||||||||||||||||||
| model = self.model | ||||||||||||||||||||||||
| _, postprocess, _ = _load_sense_voice_modules() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| # 使用 run_in_executor 来调用模型进行识别 | ||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async with MediaResolver( | ||||||||||||||||||||||||
| audio_url, | ||||||||||||||||||||||||
| media_type="audio", | ||||||||||||||||||||||||
| default_suffix=".wav", | ||||||||||||||||||||||||
| audio_url, media_type="audio", default_suffix=".wav" | ||||||||||||||||||||||||
| ).as_path(target_format="wav") as audio: | ||||||||||||||||||||||||
| res = await loop.run_in_executor( | ||||||||||||||||||||||||
| None, # 使用默认的线程池 | ||||||||||||||||||||||||
| lambda: cast(SenseVoiceSmall, self.model)( | ||||||||||||||||||||||||
| str(audio.path), language="auto", use_itn=True | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||
| lambda: model(str(audio.path), language="auto", use_itn=True), | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # res = self.model(audio_url, language="auto", use_itn=True) | ||||||||||||||||||||||||
| logger.debug(f"SenseVoice识别到的文案:{res}") | ||||||||||||||||||||||||
| text = rich_transcription_postprocess(res[0]) | ||||||||||||||||||||||||
| logger.debug(f"SenseVoice 原始识别结果: {res}") | ||||||||||||||||||||||||
| text = postprocess(res[0]) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if self.is_emotion: | ||||||||||||||||||||||||
| # 提取第二个匹配的值 | ||||||||||||||||||||||||
| matches = re.findall(r"<\|([^|]+)\|>", res[0]) | ||||||||||||||||||||||||
| if len(matches) >= 2: | ||||||||||||||||||||||||
| emotion = matches[1] | ||||||||||||||||||||||||
| text = f"(当前的情绪:{emotion}) {text}" | ||||||||||||||||||||||||
| text = f"(当前的情绪:{matches[1]}) {text}" | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| logger.warning("未能提取到情绪信息") | ||||||||||||||||||||||||
| logger.warning("未能从识别结果中提取情绪标签") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return text | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||
| logger.error(f"处理音频文件时出错: {e}") | ||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||
| logger.error(f"SenseVoice 语音识别失败: {e}", exc_info=True) | ||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk):
asyncio.run(_install_dependencies())raisesRuntimeErrorwhenever SenseVoice is initialized through the normal async provider lifecycle, becauseinitialize()already runs inside an active event loop. The exception is caught and reported as dependency-installation failure, so automatic installation never works when dependencies are missing.Triggers: When any SenseVoice dependency is not installed before provider initialization.
Suggested fix: Await
_install_dependencies()directly from an async initialization path instead of callingasyncio.run().