☰
离线语音助手实战:FunASR+DeepSeek+MeloTTS全链路教程
2026/10/7 9:44:24 网站建设 项目流程

最近有个需求一直绕不开:在一台没有外网的环境里做一套私有语音助手,声音数据一个字都不准出本机。我把FunASR、DeepSeek、离线TTS串起来,端到端跑通之后,发现这套组合其实没有想象中那么高门槛,真正花时间的反而是装环境、模块对接和交互细节。这篇教程我把每一步怎么装、每一段代码怎么写的思路全部摊开讲,适合手里有一台Linux机器、想自己折腾离线语音助手的开发者,也适合做智能家居网关、终端语音交互这类项目的朋友。

先说结论:这套链路本质就是“听懂、思考、说话”三个环节,FunASR负责听,DeepSeek负责想,TTS负责说。最难的不是任何一个模型的效果,而是把三者的接口、依赖、延迟和异常处理理顺。下面从整体架构开始,一步步来。

1. 整条语音链路先拆开看:谁在听、谁在想、谁在说

1.1 三个模块的边界

FunASR是阿里达摩院开源的一套语音识别工具包,不能简单理解成一个模型,它内部包含了多个组件。我用的组合是paraformer-zh作为主识别模型,fsmn-vad做语音活动检测,另外挂了一个ct-punc标点恢复模型。这样做的好处是:VAD先把长音频里的说话片段找出来,去掉大量静音,标点模型再把识别出来的裸文本加上逗号句号,后面给大模型的时候语义边界会清楚很多。

DeepSeek负责的是“思考”这一层。它接收FunASR输出的文本,理解用户意图,生成回复文本。这里有个容易混淆的概念:DeepSeek不是单一模型名,而是一系列模型和API服务的总称。你可以本地拉它的开源权重,也可以直接调用官方API,两者在工程上差别很大,我后面会单独讲。

TTS负责把DeepSeek回复的文本变成音频。离线场景下我主要用MeloTTS,中文发音自然度比较满意,CPU也能跑得动。工程上我把TTS封装成一个输入文本、输出wav文件的函数,不关心内部是哪个模型。

1.2 为什么接口全部用文本

整个流水线在接口层只传递两种东西:文本和音频文件路径。

麦克风音频 → [FunASR] → 文本 → [DeepSeek] → 回复文本 → [TTS] → wav文件 → 播放器

我刻意让三个模块之间不共享任何内部状态。FunASR只负责把音频变文本,DeepSeek只负责把问题变答案,TTS只负责把答案变声音。这种设计的最大好处是调试方便:任何一段输出出了问题,你都可以把中间文本打印出来,单独测某一个模块。比如发现回复不自然,那就拿同一个prompt直接测LLM,和ASR、TTS完全无关。

1.3 这套组合适合放在哪些场景

我实际把这套方案部署在办公室的Linux主机上,用途是离线的知识问答和定时提醒。更典型的场景还包括:无外网环境的工位助手、数据敏感区的语音录入终端、以及智能家居的本地语音网关。它的核心价值就两条:隐私可控、免费无限调用。和云服务比,少了很多“调用次数”的心理负担,想怎么问就怎么问。

2. 装环境之前的三个决策:硬件、TTS引擎、DeepSeek部署方式

2.1 先确认手里的机器能干什么

很多人一上来就装大模型,结果显卡扛不住,整个流程直接劝退。我建议先给机器分个档:

机器条件可行的方案预期体验
纯CPU,无GPUFunASR小模型 + TTS + DeepSeek 1.5B/7B量化版能跑通,但一句对话等待5~15秒
6~8GB显存本地7B量化模型 + 全链路GPU加速流畅,3秒内能听到回复
16GB以上显存本地14B甚至更大模型体验接近云API
没有本地算力,但有网FunASR/TTS本地 + DeepSeek官方API速度快、回复质量高,但非纯离线

我自己的主力机器是i5 + RTX 3060 12GB,跑7B量化模型比较舒服。如果你只是验证流程,CPU方案也能跑通,不要因为显卡不行就放弃,后面我会给出对应的参数调整建议。

2.2 TTS引擎选型对比

TTS是整个链路里最容易“听出来廉价感”的环节。我对比过几款主流离线方案:

引擎是否离线中文自然度部署难度适合场景
MeloTTS是好低通用助手,推荐首选
ChatTTS是很好(带语气词)中闲聊对话,但稳定性一般
Piper TTS是一般极低嵌入式、树莓派
GPT-SoVITS是非常好(可克隆音色)高需要特定音色的场景
Edge-TTS否好低能联网但不想折腾时用

这里注意Edge-TTS虽然效果不错,但本质是云服务,不满足本教程的离线前提。我最终选MeloTTS,原因很简单:中文效果好、CPU能推理、几行代码就能跑起来。ChatTTS我也测过,对话语气很丰富,甚至能模拟口头禅和笑叹词,但参数多、生成稳定性差一些,放在后面对比。

2.3 DeepSeek的三种接入方式怎么选

DeepSeek接入我把它分成三条路线:

接入方式适合人群离线成本说明
Ollama本地部署想自己掌控一切是免费一行命令拉模型,适合新手
vLLM本地部署生产环境、高并发是免费吞吐高,但配置复杂
官方API快速验证、有网否按token计费模型能力最强,接入最简单

我的建议很实际:如果你是第一次搭这套链路,先用Ollama本地部署跑通,后面即便切换到API,你的代码改动也只是一行base_url的问题。不要把某个具体接入方式焊死在你的架构里。

3. FunASR离线识别:把麦克风声音变成文字

3.1 Linux上装FunASR及可能缺的系统依赖

FunASR本身是个Python包,但Linux上装它经常栽在系统依赖上。我建议按顺序执行,避免装完才发现缺东西:

# 创建干净的Python环境,推荐3.10 conda create -n voice python=3.10 conda activate voice # 安装必要的系统级工具,Ubuntu/Debian系 sudo apt update sudo apt install -y ffmpeg portaudio19-dev # 安装Python依赖 pip install funasr modelscope torchaudio soundfile librosa

ffmpeg和portaudio19-dev是我踩坑后补上的。FunASR底层处理音频时要调librosa,而librosa解码各种格式音频依赖ffmpeg;如果你后面要接实时麦克风,sounddevice又依赖portaudio。这里一次性装好,后面能省很多事。

验证是否装好,直接进Python执行:

python -c "import funasr; print(funasr.__version__)"

正常输出版本号就说明安装成功。如果用Apple Silicon Mac,portaudio19-dev这条不适用,需要自行通过Homebrew装portaudio,其余一致。

3.2 加载识别模型并跑通第一个音频文件

环境没问题之后,写一个最简识别脚本:

from funasr import AutoModel # 第一次运行会自动下载模型,耐心等一会儿 model = AutoModel( model="paraformer-zh", vad_model="fsmn-vad", vad_kwargs={"max_single_segment_time": 30000}, punc_model="ct-punc", device="cuda:0", # 没有GPU就改成 "cpu" hub="modelscope", ) res = model.generate( input="test.wav", batch_size_s=300, hotword="小助手", ) print(res[0]["text"])

解释几个参数的意义。paraformer-zh是主识别模型,中文识别效果好,模型会从ModelScope自动下载,首次运行需要等下载完成。vad_model是语音活动检测,它的作用是先把一个长音频切分成若干“有人说话”的片段,避免在静音段浪费算力。punc_model是标点恢复,让输出带标点符号,这在后续给LLM使用时特别重要。batch_size_s控制一次批量处理的音频时长,设成300秒足够覆盖大多数录音。hotword是热词表,比如你的助手叫“小助手”,可以把这个词强化,识别率会明显提升。

注意model.generate返回的是一个列表,每个元素是一个字典,文本存在["text"]字段里。我第一次用的时候直接打印整个返回值,给调试造成了一点困惑,先提醒一下。

3.3 长音频和实时麦克风的处理思路

上面的脚本适合处理已经存在的wav文件。但语音助手场景通常是“录一段、识别一段、再录一段”,这里涉及两个问题:一是录音转存,二是静音过滤。

对于实时麦克风,我推荐一个既简单又稳定的做法:用sounddevice录音并直接保存成wav,然后把wav路径交给FunASR。示例代码:

import sounddevice as sd import soundfile as sf import numpy as np fs = 16000 duration = 3.0 # 录音时长可调 recording = sd.rec(int(fs * duration), samplerate=fs, channels=1, dtype="float32") sd.wait() sf.write("input.wav", recording, fs)

这里采样率设成16000是FunASR模型比较友好的范围。录完之后再调用model.generate识别即可。如果你想要“检测到人说话才开始识别”的效果,可以用VAD模型对音频片段做静音过滤,或者更简单地在录音过程里实时计算音量,音量低于阈值就不保存。先把基础流程跑通,再优化交互体验。

3.4 FunASR实测表现和两个高频坑

实测下来,paraformer-zh对普通话的识别准确率很能打,即使有一些环境噪声也没问题。但在两个坑让我花了不少时间:

第一个坑是首次运行下载模型太慢。解决方案是提前手动下载模型文件放到缓存目录,或者配置MODELSCOPE_CACHE环境变量来指定模型缓存位置,避免每次都在同一个目录反复下载。

第二个坑是缺少音频后端导致读取wav报错。解决方法是装soundfile和ffmpeg。别小看这个问题,代码层面不会直接告诉你缺ffmpeg,而是报一个“librosa无法解析音频”的异常,排查方向容易被带偏。

4. DeepSeek大脑:本地模型和官方API怎么选、怎么搭

4.1 本地部署路线:Ollama拉取DeepSeek模型

Ollama是目前本地部署大模型最省心的方案,它把模型下载、服务启动、API接口都封装好了。安装和启动非常简单:

# Linux一键脚本,按官方文档执行 curl -fsSL https://ollama.com/install.sh | sh # 启动服务 ollama serve

拉取模型时,去Ollama仓库搜索deepseek-r1等模型名,按你显存大小选择量化版本。以7B为例:

ollama pull deepseek-r1:7b

拉取完成后,Ollama默认监听localhost:11434,提供OpenAI兼容的HTTP接口。Python调用示例:

import requests def ask_local_llm(prompt, model="deepseek-r1:7b"): resp = requests.post( "http://localhost:11434/api/chat", json={ "model": model, "messages": [ {"role": "system", "content": "你是一个简洁的语音助手,用中文口语化回答,控制在两句话以内。"}, {"role": "user", "content": prompt}, ], "stream": False, "options": {"temperature": 0.7, "top_p": 0.9}, }, timeout=60, ) return resp.json()["message"]["content"]

这里temperature和top_p是LLM的采样参数。温度越高回复越发散,语音助手场景我建议适中偏低,0.7左右比较好,既能保证稳定又不会显得死板。timeout一定要设置,否则模型推理慢的时候请求会一直挂着,导致整个语音助手“假死”。

4.2 官方API路线:十几行代码接入

如果不想折腾显卡,或者对回复质量有更高要求,直接用官方API是最快的方式。DeepSeek API的兼容性做得比较好,直接用OpenAI SDK就能调:

from openai import OpenAI client = OpenAI( api_key="sk-你的key", base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个简洁的语音助手。"}, {"role": "user", "content": "现在几点"}, ], stream=False, ) print(resp.choices[0].message.content)

API路线的好处是模型大、推理快、不用维护本地服务,缺点也很明显:它要求有稳定的网络环境,且不是离线方案。我把它作为“快速原型”的首选,等把整个语音助手逻辑调通之后,再切换到本地模型做最终部署。

4.3 路由选择建议:用对话模型而非推理模型作为助手大脑

这点是我在真实使用中体会最深的:语音助手的“大脑”应该选对话模型,而不是推理模型。DeepSeek的推理模型(比如R1系列)在数学、逻辑题上很强,但它们的输出经常带着一大段思考过程,对语音播报来说完全是灾难——用户问一句话,模型先“内心复盘”半天,回复速度慢,输出也冗长。

语音交互对延迟极其敏感,用户说完话之后超过3秒还没有回应,就会明显觉得“卡”。所以我建议主力使用对话模型,回复质量足够,速度更快,输出更符合播报习惯。如果你只有推理模型可用,也可以通过system prompt让它不要输出思考过程,但效果不如直接用对话模型来得干净。

4.4 为语音交互定制system prompt

给LLM的system prompt是这一整套方案里性价比最高的优化点。语音和文字聊天的最大区别是:文字用户可以慢慢看长回答,语音用户听不了啰嗦内容。所以我把prompt明确写成:

你是运行在离线设备上的语音助手。 - 全部用中文回答 - 口语化、自然,像真人说话 - 回答控制在两句话以内,一般不超过40字 - 不要使用markdown、列表、括号备注 - 不要输出解释性内容

实测下来,这样一套约束能把回复长度从平均100多字压到30字左右,TTS播报时长明显缩短,整个对话节奏舒服很多。说到底,语音助手的核心体验不是“聪明”,而是“快、短、准”。

5. 离线TTS:让声音不“机械”的选型与接入

5.1 用MeloTTS把文字合成中文语音

MeloTTS是我目前在离线TTS里的首选方案。它的安装和使用都很轻量:

pip install melotts

使用代码:

from melo.api import TTS # 加载中文模型,cpu或cuda均可 model = TTS(language="ZH", device="auto") speaker_ids = model.hps.data.spk2id output_path = "reply.wav" # speed=1.0为正常语速,我建议设成1.1~1.2,听起来更自然 model.tts_to_file( "你好,我是你的离线语音助手,请问有什么需要帮助的?", speaker_ids["ZH"], output_path, speed=1.1, )

第一次运行时MeloTTS会自动下载权重文件,等一会就好。device="auto"会自动检测CUDA,如果没有GPU就回退CPU。我实测CPU模式下合成一句10字以内的回复大约1秒多,完全可接受。

关于speed参数我想多说一句:TTS默认1.0语速在中文里听着偏慢,调到1.1或者1.2之后,声音节奏更接近真人说话,也显得更“聪明”。这是我调了好几个音色参数之后得出来的结论,你可以直接抄作业。

5.2 ChatTTS和Piper在何时替换

如果MeloTTS的音色不能满足你的需求,两个备选方案值得了解。

ChatTTS的优势是对话感极强,生成的语音带有停顿、笑叹和语气变化,很接近真人聊天。但它也有明显问题:需要较长的生成时间,GPU占了也不低,而且有时候会生成奇怪的语气词,打断语义。我的建议是,做“陪伴闲聊”类产品时试ChatTTS,做“任务问答”类助手时老老实实用MeloTTS。

Piper则是极轻量路线,模型极小、CPU上运行飞快,适合树莓派这种低算力终端。中文发音自然度一般,有点机械感,但在“能说话”和“说得好听”之间,Piper明显倾斜于前者。如果碰巧你的设备功耗极低,Piper是个实用的选择。

5.3 播放端:把生成的音频真正放出来

TTS生成的是wav文件,最后一步是把它播放出来。Linux命令行最简单:

aplay reply.wav

在Python里我习惯这样写:

import subprocess def play_audio(file_path): subprocess.run(["aplay", file_path], check=True)

这样最稳,不依赖额外的Python库。如果你在Windows上,可以把命令换成powershell -c (New-Object Media.SoundPlayer 'reply.wav').PlaySync()。核心思想一样:把播放逻辑和TTS逻辑解耦,替换成本很低。

6. 把三块拼成完整助手:工程细节和延迟优化

6.1 封装三个引擎接口

前面的环境搭建和单模块验证都跑通之后,就到了工程落地阶段。我强烈建议先把三个引擎封装成独立模块,每个模块只暴露一个稳定接口:

# asr_engine.py from funasr import AutoModel class ASREngine: def __init__(self, device="cuda:0"): self.model = AutoModel( model="paraformer-zh", vad_model="fsmn-vad", punc_model="ct-punc", device=device, hub="modelscope", ) def recognize(self, wav_path): res = self.model.generate(input=wav_path) return res[0]["text"]
# llm_engine.py import requests class LLMEngine: def __init__(self, model="deepseek-r1:7b", base_url="http://localhost:11434"): self.model = model self.base_url = base_url self.system_prompt = ( "你是运行在离线设备上的语音助手。" "全部用中文回答,口语化自然," "回答控制在两句话以内,一般不超过40字。" "不要使用markdown、列表、括号,不要输出解释。" ) def chat(self, user_text): resp = requests.post( f"{self.base_url}/api/chat", json={ "model": self.model, "messages": [ {"role": "system", "content": self.system_prompt}, {"role": "user", "content": user_text}, ], "stream": False, "options": {"temperature": 0.7}, }, timeout=60, ) return resp.json()["message"]["content"]
# tts_engine.py from melo.api import TTS class TTSEngine: def __init__(self): self.model = TTS(language="ZH", device="auto") self.speaker_id = self.model.hps.data.spk2id["ZH"] def synthesize(self, text, output_path="reply.wav", speed=1.1): self.model.tts_to_file(text, self.speaker_id, output_path, speed=speed) return output_path

这样封装之后,三个模块之间完全解耦。想换DeepSeek的API?只改LLMEngine内部的chat方法就行。想换TTS?换TTSEngine一个文件即可。这个设计在后续迭代中的作用远比你想象的大。

6.2 主循环:录音、识别、生成、合成、播放

主循环代码核心逻辑就是循环:录音,识别,判断内容,请求LLM,合成语音,播放。

import sounddevice as sd import soundfile as sf import subprocess from asr_engine import ASREngine from llm_engine import LLMEngine from tts_engine import TTSEngine asr = ASREngine(device="cuda:0") llm = LLMEngine(model="deepseek-r1:7b") tts = TTSEngine() WAKE_WORD = "小助手" def record(duration=3.0, fs=16000): audio = sd.rec(int(fs * duration), samplerate=fs, channels=1, dtype="float32") sd.wait() wav_path = "input.wav" sf.write(wav_path, audio, fs) return wav_path def play(wav_path): subprocess.run(["aplay", wav_path], check=True) def main(): print("语音助手已启动,录音完成后自动识别...") while True: wav_path = record() text = asr.recognize(wav_path).strip() if not text: continue print(f"[用户] {text}") # 只有包含唤醒词才响应,避免把环境噪音当成命令 if WAKE_WORD not in text: continue # 去掉唤醒词本身,把剩余内容交给LLM clean_text = text.replace(WAKE_WORD, "", 1).strip() if not clean_text: continue reply = llm.chat(clean_text) print(f"[助手] {reply}") audio_path = tts.synthesize(reply) play(audio_path) if __name__ == "__main__": main()

这里面有一个工程细节值得注意:record()函数每次都从头开始录固定时长,实际使用中会出现录到一半才开始说话、尾部被截断的问题。想要更稳,可以改成“检测到音量超过阈值才开始保存,安静后再停”,本质上需要VAD判断。简单起见,先跑通这个版本,再把录音段换成动态截取。

6.3 加入唤醒词,避免每句话都被响应

唤醒词在离线语音助手里不只是“魔法咒语”,更是一种噪声过滤机制。如果系统对每一句识别文本都去请求LLM,那邻居说话、电视声、敲键盘声全都会被当成指令,体验极差。

我用的是文本级唤醒:把唤醒词写进FunASR的hotword参数,强化识别,然后在主循环里判断if WAKE_WORD not in text,不包含就跳过。这套方案简单可靠,缺点是需要说完唤醒词才能继续,不像“小爱同学”那样支持边听边唤醒。如果你追求更自然的全双工体验,就得引入单独的唤醒词检测模型,那属于另一个更复杂的工程,不建议第一次就上。

6.4 延迟实测与优化手段

以我的机器(i5-12400 + RTX 3060 12GB)为例,跑完整的“录音3秒 + FunASR识别 + DeepSeek 7B生成 + MeloTTS合成 + 播放”链路,延迟分布大致如下:

环节耗时
录音3秒
ASR识别0.2~0.5秒
LLM生成1~4秒
TTS合成0.3~1秒
播放语音时长

这里录音3秒是硬性的,用户不说话也在录。真正可以优化的是录音阶段:接上VAD之后,用户一开口再开始录音,能省掉大量等待。LLM生成是最大瓶颈,解决方式是换更小模型、加max_tokens限制、或者用API。TTS方面,MeloTTS已经够快,如果还不够,可以考虑把生成和播放切成异步流水线,TTS合成第一句的同时开始播放,形成“边合成边播”的效果。

7. 我在真实场景跑了两个月的几点体会

整套系统放在办公室用了一段时间,最大的感受是:技术层面的瓶颈远不如交互设计来得重要。最初我以为最麻烦的是ASR识别不准或者TTS声音难听,实际用了才发现,最难的是让用户知道什么时候该说话。按键触发和唤醒词都试过之后,我最终保留了“先录音、再判断是否包含唤醒词”的简单模式,虽然不够酷,但足够稳定,家里人也能轻松上手。

第二个体会是,CPU方案完全能跑,但体验是“能用”和“好用”的差距。如果你的主机器有NVIDIA显卡,全链路GPU加速带来的流畅度提升比换任何模型都明显。反过来,如果你只有一台旧笔记本,也不要气馁,把DeepSeek模型换成1.5B的量化版,然后接受10秒级别的响应,做定时提醒、百科问答这类场景依旧绰绰有余。

最后分享一个我一直在用的小技巧:把TTS语速调到1.1,把LLM的回复长度压缩到30字以内,整个助手的听感会一下子“聪明”起来。语音交互的体验核心不是模型多强,而是每一轮对话有多快、多短、多自然。你先把这条链路跑通,再根据实际反馈慢慢迭代,很快就能找到属于自己的最佳参数组合。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询