
1. 先明确目标这套离线语音助手到底是怎么工作的最近把家里那台吃灰的旧笔记本翻了出来折腾了一个真正能听懂人话的离线语音助手。语音识别用 FunASR意图理解用 DeepSeek回复用 TTS 合成本地语音输出整套流程跑下来最直接的感受就是没有云端的语音识别服务数据完全不出本机响应速度还比想象中快。这篇文章就把整个搭建过程掰开揉碎讲清楚从环境准备到代码落地再到问题排查照着走一遍你也能拥有一台自己的语音助手。先别急着下载代码得先把整体架构想明白。一套完整的语音助手本质上是三段管线的串联音频进、文字处理、语音出。具体到技术栈就是 ASR自动语音识别把麦克风采集到的声音转成文字LLM大语言模型理解这段文字并生成回复内容TTS语音合成把回复文本重新变回语音播放出来。三部分缺一不可任何一环拉胯整个体验都会崩。1.1 一条语音指令的完整旅程为了让你有个直观感受我拿一条真实指令完整走一遍。假设你对麦克风说帮我查一下明天北京的天气。第一步麦克风采集到的 PCM 音频数据进入 FunASR经过 VAD语音活动检测切分有效片段再送入声学模型识别成文字输出结果是帮我查一下明天北京的天气。这个过程完全在本地 CPU/GPU 上完成没有任何数据上传到云端。第二步这段文字作为 user 消息发给 DeepSeek。你可以选择调用 DeepSeek 的云端 API也可以本地部署蒸馏版模型。DeepSeek 理解你是在询问天气根据预设的 system prompt 生成一段结构化的回复文本比如明天北京晴转多云白天最高温度 12 摄氏度夜间最低 3 摄氏度建议穿厚外套。第三步这段回复文本交给 TTS 引擎。TTS 把文本编码成音素序列再通过声学模型和声码器生成音频波形最后通过扬声器播放出来。三条管线加起来理想情况下端到端延迟控制在两秒以内用户几乎感知不到中间经历了三个不同系统的协作。1.2 为什么选 FunASR DeepSeek TTS 这套组合市面上做语音助手的方案其实不少有纯开源的也有商业闭源的我之所以选这套组合核心考量的三个维度分别是离线能力、中文识别效果、以及二次开发的灵活度。先说 FunASR。它是阿里通义实验室开源的一个语音识别工具包底层模型用的是 Paraformer 系列。Paraformer 是非自回归的端到端模型什么意思呢传统的自回归模型是一个字一个字地吐出识别结果速度慢且依赖上下文非自回归模型是一次性并行预测整段文字延迟大幅降低。实测下来在普通笔记本 CPU 上识别一段三五秒的语音基本能做到实时甚至超实时这在离线场景非常重要。而且 Paraformer 的中文识别准确率在开源模型里属于第一梯队还自带标点预测和后处理输出直接可读不用自己再去拼一个语言模型做纠错。DeepSeek 的选择逻辑更简单。它提供了和 OpenAI 兼容的 API 接口这意味着我不需要学习新的 SDK 调用方式用 openai 库改一下 base_url 就能跑起来。更重要的是DeepSeek 对中文语义的理解能力很强作为语音助手的大脑它承担的是意图理解、任务规划和文本生成的职责中文场景下的表现直接影响整体体验。此外DeepSeek 也支持本地部署如果你追求完全离线可以拉取蒸馏后的小参数模型跑在本地机器上后面我会对比这两种方式的取舍。TTS 环节我试过好几个方案包括 Coqui TTS、Piper 和 PaddleSpeech最终选了最适合自己场景的。Coqui TTS 的中文模型质量不错但依赖较重Piper 轻量、启动快、支持中文音色适合嵌入式场景PaddleSpeech 功能全但安装依赖复杂。我的建议是如果你机器性能一般优先选 Piper资源占用极小合成速度非常快如果追求更高的音质和自然度Coqui TTS 的中文模型更合适。这套组合的另一个优势是三个组件都有活跃的开源社区遇到问题基本都能在 GitHub issues 里找到解决方案。2. 环境准备从零开始装好所有依赖2.1 硬件到底要多高配先说结论如果只是做一个能用的语音助手不需要多好的硬件。我的测试机是一台 2018 年的老笔记本i5-8250U 处理器、8GB 内存、没有独立显卡跑整个过程完全没有问题。如果你有 NVIDIA 显卡CUDA 加速下 FunASR 的识别速度会更快TTS 合成也基本无感但没有 GPU 也能流畅运行只是延迟略高一点。需要重点关注的其实是内存和磁盘空间。FunASR 的模型文件编译后大约 200300MBTTS 的模型从几十 MBPiper到几百 MBCoqui不等DeepSeek 本地部署的模型将来可能需要几 GB 甚至十几 GB 的存储空间所以建议系统盘至少预留 20GB 空闲空间。内存方面8GB 是及格线如果同时跑 ASR 模型和 TTS 引擎再多开几个浏览器标签页内存可能吃紧有条件的话建议 16GB。操作系统我推荐 Ubuntu 22.04 LTS 或者 Debian 12Python 环境用 3.10 或 3.11 版本。Windows 用户也能跑但音频采集模块和某些依赖在 Linux 上更省心所以如果你有闲置的 Linux 机器直接拿来做语音助手是再合适不过的选择。2.2 FunASR 安装与首个识别测试FunASR 的安装比想象中简单核心就是 pip 一条命令。我推荐用虚拟环境来隔离依赖避免污染系统 Python 环境。python3 -m venv voice_assistant source voice_assistant/bin/activate pip install funasr modelscope torchaudio这里有必要解释一下为什么需要 modelscope。FunASR 的模型默认从 ModelScope 模型库拉取类似 Hugging Face 在国内的生态安装 modelscope 是为了能自动下载和缓存模型文件。如果你网络环境特殊也可以手动把模型文件下载好放到本地目录然后通过 AutoModel 指定模型路径加载。装完之后先跑一个最小测试验证安装是否正确。找一段中文语音的 wav 文件可以用手机录一段自己的语音转成 16kHz 采样率单声道的 wav然后用下面的代码识别from funasr import AutoModel model AutoModel( modelparaformer-zh, model_revisionv2.0.4, vad_modelfsmn-vad, punc_modelct-punc, ) result model.generate(inputtest.wav) print(result[0][text])第一次运行会自动下载模型文件网速正常的话几分钟内完成。需要注意的一个细节是model_revision参数不同版本的模型权重在 ModelScope 上有对应的 tag如果不指定 revision可能会拉到旧版本导致识别效果不理想。我实测下来v2.0.4是目前比较稳定的版本。识别测试如果输出了一段通顺的中文文本说明 ASR 环节已经通了。这里顺带提一句FunASR 的 AutoModel 初始化时可以同时挂载 VAD 模型和标点模型VAD 的作用是自动检测音频中的有效语音片段避免把静音和噪音也拿去识别标点模型则是给识别结果加标点符号让输出更可读。这两个附属模型对最终体验的提升非常明显建议一定要带上。2.3 DeepSeek 两种接入方式怎么选DeepSeek 提供两种使用方式云端 API 和本地部署。这两者的选择直接决定了你的语音助手是半离线还是全离线。云端 API 是最快能跑起来的方式。你需要去 DeepSeek 开放平台注册账号创建 API Key然后就可以通过标准的 OpenAI 兼容接口调用了。base_url 是https://api.deepseek.com模型名是deepseek-chat。这种方式的好处是零部署成本用的是满血版大模型理解能力最强坏处是必须联网每次对话内容会发送到云端处理严格来说不算完全离线。本地部署则是把模型跑在自己的机器上数据彻底不出门。DeepSeek 官方发布过一系列开源模型包括不同参数量的版本本地部署通常选 7B、14B 或者 32B 的蒸馏版本。但对于没有独显的普通笔记本7B 模型跑起来会比较吃力量化后的模型也需要 8GB 以上的内存而且推理速度是秒级响应和云端 API 的体验差距明显。我的建议是分阶段推进前期先用云端 API 把整个管线的逻辑跑通验证语音助手的交互流程后续如果确实有隐私要求或者断网场景再考虑本地部署。毕竟架构上两者唯一的变化就是 LLM 调用层换一个实现其他环节完全不用动。3. 逐环节实现识别、理解、回复三步走3.1 用 FunASR 把声音变成文字真正落地的时候语音识别不只是调一个模型这么简单你还要解决音频从哪里来、格式怎么转换、识别完了怎么处理结果的问题。音频采集我用的sounddevice库它可以接管麦克风以流式方式把音频数据喂给后续处理。采集时有个关键参数必须注意采样率。FunASR 的模型接受 16kHz 采样率的单声道音频而麦克风默认往往是 44.1kHz 或 48kHz 的双声道如果直接把原始采样数据丢给模型识别结果会一塌糊涂。所以要么采集时就设置samplerate16000要么采集后做重采样。下面是一个稳定的采集加识别的最小示例import sounddevice as sd import numpy as np from funasr import AutoModel model AutoModel( modelparaformer-zh, vad_modelfsmn-vad, punc_modelct-punc, ) def record_audio(duration5, samplerate16000): audio sd.rec(int(duration * samplerate), sampleratesamplerate, channels1, dtypeint16) sd.wait() return audio.flatten() def recognize(audio): result model.generate(inputaudio) return result[0][text] # 录 5 秒音频并识别 audio record_audio() text recognize(audio) print(识别结果:, text)注意这里model.generate可以直接接收 numpy 数组作为输入不需要保存成文件再读取省去了大量临时文件读写。如果你需要保存现场音频再单独落盘即可。还有一个小细节麦克风采集到的 int16 数据可以直接作为 FunASR 的输入但如果你从其他来源拿到的是 float32 的音频需要确认数值范围是否在 -1.01.0 之间还是 -3276832767 之间类型不匹配会导致识别结果变成乱码。3.2 用 DeepSeek 生成回复内容LLM 接入这块代码量其实最少但最容易踩坑的是 API 参数的配置。下面这段代码演示了以 OpenAI 兼容方式调用 DeepSeek API 的完整流程from openai import OpenAI client OpenAI( api_keyyour_api_key_here, base_urlhttps://api.deepseek.com ) def chat_with_ai(user_text): system_prompt ( 你是一个放在用户本地的语音助手。你的回复必须简洁自然 适合用语音播报避免使用列表符号、表格和 Markdown 格式。 回复长度控制在 100 字以内语气口语化。 ) response client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: system_prompt}, {role: user, content: user_text}, ], max_tokens200, temperature0.7, streamFalse, ) return response.choices[0].message.content这里有个地方我需要特别强调那就是 system prompt 的设计直接影响 TTS 播报的效果。如果你不告诉模型回复要适合语音播报DeepSeek 会倾向于输出带 Markdown 标题、列表、加粗标记的文本TTS 引擎读到这些特殊符号时要么直接念出井号星号等字符要么直接报错。加了上面的约束之后输出就是干净的口语化文本播报体验立竿见影。另外如果你走本地部署路线调用层其实基本不用改。本地部署通常用 llama.cpp 或者 vLLM 起一个兼容 OpenAI 接口的服务然后你把上面的base_url改成http://localhost:8000/v1模型名改成你部署的模型名即可。这也是我不厌其烦地强调用 OpenAI 兼容格式原因——上层代码完全复用切换后端零成本。3.3 用 TTS 合成语音播报TTS 的选择上我给两条路线都写一下。先看 Piper它走的是命令行和 Python 绑定两种方式模型格式是优化的 ONNX在 CPU 上跑的飞快而且专门为中文训练了多种音色模型。import subprocess import sounddevice as sd import numpy as np def piper_tts(text, model_pathzh_CN-huayan-medium.onnx): # 用 Piper 合成 WAV输出到 stdout process subprocess.run( [piper, --model, model_path, --output_raw], inputtext.encode(utf-8), capture_outputTrue, ) audio np.frombuffer(process.stdout, dtypenp.int16) sd.play(audio, samplerate22050) sd.wait()这里有个参数要注意--output_raw输出的是裸 PCM 数据你需要在播放时明确告诉 sounddevice 采样率。Piper 中文模型的默认采样率是 22050Hz和 FunASR 的 16kHz 不同播放时别搞混。如果你追求更自然的音色可以换 Coqui TTS。Coqui 支持中文的 TTS 模型用的是 VITS 架构合成质量比 Piper 高一个档次但依赖 PyTorch 全家桶安装体积大CPU 推理速度也偏慢。我的建议是日常快速播报用 Piper重要场景或者演示时切到 Coqui。两种方案切换只需改一行代码不需要动其他逻辑。4. 串起来完整主控代码与实测调优4.1 主控逻辑与完整代码三个环节单独跑通之后最后就是把它们串成一个完整应用。主控逻辑的核心是一个主循环等待用户说话 - 录音 - 识别 - 生成回复 - 语音播报 - 回到等待状态。import time import sounddevice as sd import numpy as np from funasr import AutoModel from openai import OpenAI # 初始化 ASR asr_model AutoModel( modelparaformer-zh, model_revisionv2.0.4, vad_modelfsmn-vad, punc_modelct-punc, ) # 初始化 DeepSeek 客户端 llm_client OpenAI( api_keyyour_api_key_here, base_urlhttps://api.deepseek.com ) SAMPLERATE 16000 CHANNELS 1 def record_audio(duration5): audio sd.rec(int(duration * SAMPLERATE), samplerateSAMPLERATE, channelsCHANNELS, dtypeint16) sd.wait() return audio.flatten() def asr_recognize(audio): result asr_model.generate(inputaudio) return result[0][text] if result else def llm_reply(user_text): response llm_client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是本地语音助手回复简洁口语化适合语音播报不超过100字不用Markdown。}, {role: user, content: user_text}, ], max_tokens200, temperature0.7, ) return response.choices[0].message.content def tts_speak(text): subprocess.run( [piper, --model, zh_CN-huayan-medium.onnx, --output_raw], inputtext.encode(utf-8), capture_outputTrue, ) # 实际使用时把 stdout 转成音频播放 def main_loop(): print(语音助手已启动请说话...) while True: try: audio record_audio(5) text asr_recognize(audio) if not text: continue print(f识别: {text}) if 退出 in text or 再见 in text: print(语音助手退出) break reply llm_reply(text) print(f回复: {reply}) tts_speak(reply) except KeyboardInterrupt: break if __name__ __main__: main_loop()这段代码是核心骨架直接把三个环节拼接起来跑通了。实际使用时会发现一个问题固定录 5 秒太死板了有的人一句话说长了录不完有的人两秒说完要干等三秒。更好的方案是用 VAD 检测静音检测到用户停止说话就立刻结束录音。这个逻辑需要把声卡的音频数据分块读取然后交给 VAD 模型判断实现起来稍复杂但体验提升明显。4.2 实测效果与参数调优把代码跑起来之后我做了几轮实测记录了一些关键数据供你参考。测试环境是 i5-8250U 8GB 内存无独立显卡。环节耗时首次耗时热身后备注FunASR 加载810 秒0.5 秒首次需加载模型到内存识别 5 秒语音12 秒0.81.5 秒CPU 推理DeepSeek API 响应0.51.5 秒同上取决于网络Piper TTS 合成100300 毫秒同上1030 字短句整体端到端延迟在 2.54.5 秒之间这个数字对于离线语音助手来说属于可接受范围但还有不少优化空间。我实际调优过程中发现几个关键点。第一FunASR 的batch_size参数可以调大如果你一次性传入多个音频片段批量推理能显著提升吞吐但在单条对话场景下影响不大。第二DeepSeek API 的max_tokens设置不能太高语音回复超过 100 字念起来很啰嗦而且 TTS 合成时间也会变长。第三Piper 模型可以预先加载到内存避免每次合成都从磁盘读模型启动速度能提升不少。5. 踩坑实录常见问题与排查手册5.1 识别速度慢、结果不准识别不准是最常见的问题而且原因有好几种很多初次上手的朋友根本分不清是哪里出了问题。我按照排查优先级整理了最常见的情况。首先是采样率不匹配。我见过很多人用 48kHz 的音频直接喂给 FunASR识别结果完全不可用。遇到这类问题先检查采集端采样率是否设置为 16000或者用librosa.load(path, sr16000)重新采样后再识别。其次是环境噪音干扰。Paraformer 模型虽然抗噪性还可以但如果你在风扇呼啸的笔记本旁边测试识别准确率下降是很正常的。解决思路有两个一是录制时尽量靠近麦克风二是对采集到的音频做降噪预处理。我自己用了一个比较简单的方法先经过 VAD 切割出有效语音段再对这段音频做高通滤波滤掉低频噪声效果不错。最后是方言和专有名词。Paraformer 对标准普通话识别很准但遇到方言口音或者生僻专有名词就容易出错。这时候可以启用 FunASR 的热词增强功能通过hotword参数传入自定义词表模型会对这些词优先匹配。比如你的场景经常出现X 项目这类专有名词把它们加进热词表识别准确率能提升好几个百分点。5.2 音频采集异常sounddevice 采集音频时最容易碰到的坑有两个。第一个是InputStream not available这类权限错误在 Linux 上没有给当前用户音频设备访问权限导致的解决方案是把用户加入audio用户组sudo usermod -aG audio $USER然后注销重新登录。第二个是 ALSA 配置问题如果你在 Linux 下玩过音频就会知道ALSA 的~/.asoundrc配置不当会导致各种奇奇怪怪的设备错误甚至直接崩溃排查方式是把配置文件先备份后删掉让系统用默认配置试试。5.3 资源占用过高的优化方案同时在内存里驻扎 ASR 模型、TTS 引擎和 LLM 客户端小内存机器确实会吃不消。我实测过程中用htop监控过内存使用FunASR 的 Paraformer 模型加载后大约占 600MBPiper 模型占 150MBPython 进程基础占用 300MB整体下来 1.2GB 左右。如果你的机器内存只有 4GB就得做减法。三个优化方向。第一只用必要的模型。FunASR 的vad_model和punc_model虽然有用但如果内存吃紧可以先去掉只留核心识别模型。第二用更小的模型变体。Paraformer 有paraformer-zh-small版本模型体积更小虽然准确率略降但对资源和速度友好得多。第三TTS 选用 Piper 而不是 Coqui节省大几百 MB 的内存开销。如果要做成 7x24 小时运行的服务还可以考虑异步加载空闲一段时间后把模型从内存中卸载。还有一点值得一提就是模型的缓存目录。FunASR 和 ModelScope 默认会把模型下载到~/.cache/modelscope目录如果你磁盘空间紧张可以设置MODELSCOPE_CACHE环境变量把缓存指到其他盘。6. 还能怎么玩扩展思路与个人心得6.1 扩展场景语音助手跑通之后能扩展的方向其实非常多。我目前已经接了几个实用场景其中一个是用热词唤醒替代手动按键触发。音频采集默认是全时监听的通过一个唤醒词模型比如可以用 openWakeWord 或者自训练的模型检测到你好小助之类的唤醒词后才触发后面的 ASR 识别流程这样既省电又不会把无关对话误判成指令。另一个扩展方向是把回复内容接进自动化系统。比如当你问家里的温度怎么样时DeepSeek 不只是生成文本而是解析成结构化指令调用传感器读取温度再把结果拼进回复文本。这种思路本质上把语音助手变成了家庭自动化的自然语言入口价值比单纯聊大天高得多。如果你有兴趣可以在 system prompt 里约定输出 JSON 格式让模型返回一个指令结构然后后端解析执行这算是一个比较标准的 LLM Agent 雏形。我还试过把 ASR 识别出的文本流式地送进日志系统这样语音助手处理过的每一条请求都可以回溯查看。出错的时候翻日志定位比盲调代码高效得多。6.2 个人心得折腾完这一整套东西我最大的感受是离线语音助手的技术门槛已经低到了普通开发者都能独立完成的程度真正的难点反而在细节调优上——怎么让回复更口语化、怎么处理各种音频异常、怎么在资源受限的机器上把延迟压下来。我的一个建议是第一次搭建别追求完美先把最简管线跑通哪怕是固定录音 5 秒、回复冗长一点、合成音质一般都能接受。端到端跑通带来的正反馈比对着文档调三天参数更有效。等整体流程验证没问题了再逐步换更好的模型、加上 VAD 自动断句、优化提示词一步一步打磨。最后再分享一个小技巧调试 TTS 的时候别每次都完整走一遍 ASR LLM 的流程太慢。可以先把 DeepSeek 的回复存成文本文件单独调试 TTS 环节等音色和速度满意了再串回去。分段调试一次只动一个环节出问题能立刻定位这是我在整个项目里最受用的习惯。