免费本地部署TTS+STT+LLM三合一语音AI方案
在开发智能应用时,语音交互和自然语言理解是两大核心能力。然而,将文本转语音(TTS)、语音转文本(STT)和大语言模型(LLM)三者无缝集成,常常面临成本高昂、接口复杂、网络依赖强等难题。本文旨在分享一套完全免费、可本地部署的“三合一”整合方案,涵盖从环境搭建、核心代码实现到生产优化的全流程。无论你是想为个人项目添加语音助手功能,还是为企业级应用探索低成本AI方案,都能从本文获得可直接复用的代码和清晰的配置思路。
1. 背景与核心概念:为什么需要 TTS、STT 与 LLM 的整合?
在构建具备自然交互能力的AI应用时,单一的文本或语音接口往往无法满足复杂场景的需求。一个完整的智能对话闭环通常包含“听、想、说”三个环节,这正是STT、LLM和TTS三种技术协同工作的结果。
STT(语音转文本):负责将用户输入的音频信号转换为计算机可处理的文本。它是人机交互的“耳朵”,其准确度直接决定了后续流程的输入质量。常见的应用场景包括语音助手、会议转录、语音搜索等。
LLM(大语言模型):作为系统的“大脑”,它负责理解STT转换后的文本,进行意图识别、上下文分析、知识问答、内容生成等复杂的自然语言处理任务。LLM的能力决定了交互的智能程度。
TTS(文本转语音):将LLM生成的文本回复,转换为自然流畅的语音输出。它是人机交互的“嘴巴”,其自然度和情感表现力极大地影响用户体验,常用于智能客服、有声内容播报、导航提示等。
将三者整合的优势在于:
- 成本可控:利用开源或免费的API,避免对昂贵商业服务的依赖。
- 隐私安全:支持本地化部署,敏感音频和对话数据无需上传至第三方。
- 高度定制:可根据具体场景,自由选择和微调每一环节的模型或引擎。
- 流程闭环:构建从语音输入到语音输出的端到端自动化流程,提升产品完整性。
接下来,我们将从环境准备开始,一步步实现这个“三合一”系统。
2. 环境准备与版本说明
本方案设计为跨平台,核心逻辑使用Python实现,因其在AI和语音处理领域有丰富的生态库支持。我们将选用当前(以常见稳定版本为例)广泛使用的开源工具和框架。
基础运行环境:
- 操作系统:Ubuntu 20.04/22.04 LTS, Windows 10/11, macOS Monterey 及以上(本文以Ubuntu为例,命令略有差异)。
- Python:版本 3.8 - 3.10。推荐使用 3.8 或 3.9 以保证库的最佳兼容性。
- 包管理工具:
pip(>=20.0) - 虚拟环境:强烈建议使用
venv或conda创建独立环境,避免包冲突。
核心组件选型与版本:为了达成“免费”和“可本地化”的目标,我们进行如下选型:
- STT引擎:
Vosk。这是一个离线的开源语音识别工具包,支持多种语言,识别准确度高,且对硬件要求相对友好。我们将使用其Python接口。 - TTS引擎:
Edge-TTS命令行工具 +pyttsx3备选。Edge-TTS:通过命令行调用微软Edge浏览器的在线TTS服务,声音质量高、自然度好,且目前免费。虽然需要网络,但API稳定,是快速获得高质量语音的优选。pyttsx3:一个纯离线的TTS库,跨平台,支持系统自带的语音引擎(如Windows的SAPI5,Linux的espeak)。作为离线备选方案。
- LLM接口:
Ollama+LangChain。Ollama:一个强大的工具,可以让你在本地轻松运行、管理和与各种开源大语言模型(如Llama 2, Mistral, Gemma等)交互。LangChain:一个用于开发由LLM驱动的应用程序的框架。我们将用它来标准化与Ollama的交互,并方便未来扩展。
项目结构预览:在开始前,我们先规划一下项目目录,这有助于理解代码组织。
tts_stt_llm_demo/ ├── main.py # 主程序入口 ├── config.yaml # 配置文件(可选) ├── requirements.txt # Python依赖列表 ├── models/ # 存放Vosk离线模型 │ └── vosk-model-small-en-us-0.15/ # 英文小模型 ├── audio_cache/ # 缓存生成的TTS音频文件 └── README.md3. 核心组件安装与配置
3.1 创建Python虚拟环境与安装基础依赖
首先,创建并激活一个干净的Python环境。
# 创建项目目录并进入 mkdir tts_stt_llm_demo && cd tts_stt_llm_demo # 创建虚拟环境(以venv为例) python3 -m venv venv # 激活虚拟环境 # Linux/macOS source venv/bin/activate # Windows # venv\Scripts\activate # 升级pip pip install --upgrade pip创建requirements.txt文件,并写入以下依赖:
# requirements.txt vosk==0.3.45 sounddevice==0.4.6 # 用于录音 numpy==1.24.3 # 音频处理常用库 langchain==0.1.0 langchain-community==0.0.10 pyttsx3==2.90 requests==2.31.0 # 用于可能的HTTP请求 PyYAML==6.0 # 用于读取配置然后安装它们:
pip install -r requirements.txt注意:Edge-TTS我们稍后通过命令行安装,因为它主要是一个命令行工具。
3.2 配置 Vosk (STT) 离线模型
Vosk需要下载对应的语音识别模型。我们以轻量级的英文小模型为例。
- 访问 Vosk模型下载页面 。
- 找到
vosk-model-small-en-us-0.15(约40MB) 并下载压缩包。 - 将压缩包解压到项目根目录的
models文件夹下。最终路径应为./models/vosk-model-small-en-us-0.15。
你也可以使用其他更大更精确的模型,如中文模型vosk-model-small-cn-0.22。
3.3 安装与验证 Ollama (LLM)
Ollama的安装非常简单。
# Linux/macOS 一键安装 curl -fsSL https://ollama.com/install.sh | sh # Windows: 直接从官网 https://ollama.com 下载安装程序并运行。安装完成后,启动Ollama服务(通常安装后会自动运行)。然后,拉取一个开源模型,例如轻量级的llama2:7b或更高效的mistral:7b。
# 拉取模型(首次运行需要下载,时间较长) ollama pull mistral:7b # 验证模型是否运行,进行简单对话 ollama run mistral:7b在出现的提示符后输入Hello, how are you?,如果模型能回复,说明LLM环境就绪。按Ctrl+D退出交互模式。
3.4 安装 Edge-TTS
Edge-TTS是一个Python包,但它主要通过命令行调用。
# 在之前激活的虚拟环境中安装 pip install edge-tts安装后,可以在终端测试:
# 生成一个语音文件 edge-tts --text "Hello, this is a test." --write-media hello_test.mp3如果生成了hello_test.mp3文件并能播放,说明TTS组件工作正常。
4. 核心代码实现:构建“三合一”管道
我们将按照录音(STT) -> 理解(LLM) -> 回复(TTS)的流程编写核心代码。
4.1 实现语音识别 (STT with Vosk)
创建stt_module.py文件,实现录音和识别功能。
# stt_module.py import json import queue import sys import sounddevice as sd from vosk import Model, KaldiRecognizer class SpeechToText: def __init__(self, model_path="./models/vosk-model-small-en-us-0.15"): """ 初始化Vosk识别器。 :param model_path: Vosk离线模型的路径 """ try: self.model = Model(model_path) except Exception as e: print(f"无法加载Vosk模型,请检查路径: {model_path}") print(f"错误信息: {e}") sys.exit(1) self.recognizer = KaldiRecognizer(self.model, 16000) self.sample_rate = 16000 self.audio_queue = queue.Queue() def audio_callback(self, indata, frames, time, status): """声音输入回调函数,将音频数据放入队列。""" if status: print(f"音频输入状态: {status}", file=sys.stderr) self.audio_queue.put(bytes(indata)) def listen_and_transcribe(self, duration=5): """ 录制一段音频并将其转换为文本。 :param duration: 录音时长(秒) :return: 识别出的文本字符串,失败返回None """ print(f"\n🎤 请开始说话(录音 {duration} 秒)...") transcription = None # 开始录音流 with sd.RawInputStream(samplerate=self.sample_rate, blocksize=8000, dtype='int16', channels=1, callback=self.audio_callback): sd.sleep(duration * 1000) # 录音时长 print("录音结束,处理中...") # 处理队列中的音频数据 while not self.audio_queue.empty(): data = self.audio_queue.get() if self.recognizer.AcceptWaveform(data): result = json.loads(self.recognizer.Result()) transcription = result.get("text", "") else: # 获取部分结果(实时反馈用) partial_result = json.loads(self.recognizer.PartialResult()) # print(f"实时识别: {partial_result.get('partial', '')}") # 可选:打印实时识别 if transcription and len(transcription.strip()) > 0: print(f"✅ 识别结果: {transcription}") return transcription.strip() else: print("❌ 未识别到有效语音。") return None if __name__ == "__main__": # 模块测试代码 stt_engine = SpeechToText() text = stt_engine.listen_and_transcribe(duration=3) if text: print(f"最终文本: {text}")4.2 实现大语言模型交互 (LLM with Ollama & LangChain)
创建llm_module.py文件,使用 LangChain 标准化调用 Ollama。
# llm_module.py from langchain_community.llms import Ollama from langchain.prompts import PromptTemplate from langchain.chains import LLMChain class LanguageModelProcessor: def __init__(self, model_name="mistral:7b"): """ 初始化LangChain的Ollama LLM链。 :param model_name: Ollama中已拉取的模型名称 """ # 初始化Ollama LLM,指定基础URL(默认本地11434端口) self.llm = Ollama(model=model_name, base_url="http://localhost:11434") # 定义一个提示模板,让模型扮演一个友好的助手 prompt_template = """ 你是一个有帮助的AI助手。请用简洁、友好的方式回答用户的问题。 如果问题不清楚,可以请求澄清。 用户问题:{user_input} 助手回答: """ self.prompt = PromptTemplate( input_variables=["user_input"], template=prompt_template ) # 创建链 self.chain = LLMChain(llm=self.llm, prompt=self.prompt) def generate_response(self, user_input): """ 根据用户输入生成回复。 :param user_input: 用户输入的文本 :return: 模型生成的回复文本 """ if not user_input or len(user_input.strip()) == 0: return "我没有听到您说什么。" print(f"🧠 思考中... (输入: {user_input})") try: response = self.chain.run(user_input=user_input) print(f"🤖 生成回复: {response}") return response.strip() except Exception as e: print(f"LLM调用出错: {e}") return "抱歉,我现在有点困惑,请稍后再试。" if __name__ == "__main__": # 模块测试代码 llm_processor = LanguageModelProcessor() test_input = "What is the capital of France?" answer = llm_processor.generate_response(test_input) print(f"测试回复: {answer}")4.3 实现文本转语音 (TTS with Edge-TTS)
创建tts_module.py文件,封装 Edge-TTS 的命令行调用。
# tts_module.py import subprocess import os import pyttsx3 from pathlib import Path class TextToSpeech: def __init__(self, use_edge_tts=True, voice="en-US-JennyNeural", cache_dir="./audio_cache"): """ 初始化TTS引擎。 :param use_edge_tts: 是否使用Edge-TTS(True),否则使用pyttsx3离线引擎(False) :param voice: Edge-TTS的语音名称(如 en-US-JennyNeural, zh-CN-XiaoxiaoNeural) :param cache_dir: 音频缓存目录 """ self.use_edge_tts = use_edge_tts self.voice = voice self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(exist_ok=True) # 确保缓存目录存在 if not use_edge_tts: # 初始化离线引擎 self.offline_engine = pyttsx3.init() # 设置语速、音量等属性(可选) self.offline_engine.setProperty('rate', 150) self.offline_engine.setProperty('volume', 0.9) def speak(self, text, save_to_file=None): """ 将文本转换为语音并播放/保存。 :param text: 要合成的文本 :param save_to_file: 如果提供,则保存为文件(如 output.mp3),否则直接播放 :return: 生成的音频文件路径(如果保存了的话) """ if not text: print("⚠️ 文本为空,无法合成语音。") return None print(f"🔊 正在合成语音: {text[:50]}...") if self.use_edge_tts: # 使用Edge-TTS return self._speak_with_edge_tts(text, save_to_file) else: # 使用pyttsx3离线引擎 return self._speak_with_pyttsx3(text, save_to_file) def _speak_with_edge_tts(self, text, save_to_file): """使用Edge-TTS合成语音。""" import edge_tts import asyncio async def _async_speak(): communicate = edge_tts.Communicate(text, self.voice) if save_to_file: output_path = self.cache_dir / save_to_file if not os.path.isabs(save_to_file) else Path(save_to_file) await communicate.save(str(output_path)) print(f"✅ 语音已保存至: {output_path}") return str(output_path) else: # 直接播放(需要系统有播放mp3的能力) player = edge_tts.AudioPlayer() await communicate.stream() async for chunk in communicate.stream(): if chunk["type"] == "audio": player.add(chunk["data"]) await player.play() print("✅ 语音播放完毕。") return None try: return asyncio.run(_async_speak()) except Exception as e: print(f"Edge-TTS合成失败: {e}") # 降级到离线引擎 print("尝试使用离线引擎...") self.use_edge_tts = False return self._speak_with_pyttsx3(text, save_to_file) def _speak_with_pyttsx3(self, text, save_to_file): """使用pyttsx3离线合成语音。""" try: if save_to_file: output_path = self.cache_dir / save_to_file if not os.path.isabs(save_to_file) else Path(save_to_file) self.offline_engine.save_to_file(text, str(output_path)) self.offline_engine.runAndWait() print(f"✅ 离线语音已保存至: {output_path}") return str(output_path) else: self.offline_engine.say(text) self.offline_engine.runAndWait() print("✅ 离线语音播放完毕。") return None except Exception as e: print(f"离线TTS合成失败: {e}") return None if __name__ == "__main__": # 模块测试代码 - 使用Edge-TTS tts_online = TextToSpeech(use_edge_tts=True, voice="en-US-JennyNeural") tts_online.speak("Hello, this is a test of the text to speech system.", save_to_file="test_online.mp3") # 模块测试代码 - 使用离线引擎 tts_offline = TextToSpeech(use_edge_tts=False) tts_offline.speak("This is an offline test.", save_to_file="test_offline.wav")4.4 集成主程序
最后,创建main.py文件,将三个模块串联起来,形成一个完整的交互循环。
# main.py import time from stt_module import SpeechToText from llm_module import LanguageModelProcessor from tts_module import TextToSpeech def main_loop(): """主交互循环""" print("=" * 50) print("🚀 免费 TTS + STT + LLM 三合一系统启动") print("=" * 50) # 1. 初始化各个模块 print("[1/3] 初始化语音识别模块 (STT)...") stt = SpeechToText(model_path="./models/vosk-model-small-en-us-0.15") print("[2/3] 初始化语言模型模块 (LLM)...") # 请确保Ollama服务正在运行,且模型名正确 llm = LanguageModelProcessor(model_name="mistral:7b") # 或 "llama2:7b" print("[3/3] 初始化语音合成模块 (TTS)...") # 使用Edge-TTS在线引擎,语音选择美式英语Jenny tts = TextToSpeech(use_edge_tts=True, voice="en-US-JennyNeural") print("\n✅ 所有模块初始化完成!") print("💡 提示:系统将循环进行‘听你说 -> 思考 -> 回答’。") print(" 输入 'quit', 'exit' 或按下 Ctrl+C 退出程序。\n") # 2. 开始交互循环 while True: try: # 步骤A: 语音输入与识别 user_text = stt.listen_and_transcribe(duration=5) # 录音5秒 if not user_text: continue # 没识别到内容,重新开始循环 # 简单的退出命令判断(识别出的文本) if user_text.lower() in ['quit', 'exit', 'stop', '退出']: print("收到退出指令,再见!") tts.speak("Goodbye!") break # 步骤B: 大语言模型处理 bot_response = llm.generate_response(user_text) # 步骤C: 语音输出 # 为了演示,我们将回复保存为文件并播放 timestamp = int(time.time()) audio_filename = f"response_{timestamp}.mp3" tts.speak(bot_response, save_to_file=audio_filename) print("-" * 30) # 分隔每次对话 except KeyboardInterrupt: print("\n\n程序被用户中断。") break except Exception as e: print(f"\n⚠️ 主循环发生未知错误: {e}") print("尝试继续运行...") time.sleep(1) # 避免错误循环导致CPU占用过高 if __name__ == "__main__": main_loop()5. 运行与验证
确保所有步骤已完成,并且Ollama服务在后台运行。
启动Ollama服务(如果未运行):
ollama serve # 或者直接运行模型也会启动服务 # ollama run mistral:7b在项目根目录下运行主程序:
python main.py预期交互流程:
- 程序启动,初始化三个模块。
- 提示“请开始说话(录音5秒)...”,此时对着麦克风清晰地说一句英文,例如 “What is the weather like today?”
- 录音结束后,Vosk会识别文本并打印。
- LangChain调用本地的Mistral模型进行思考,生成回复文本并打印。
- Edge-TTS将回复文本合成语音,保存为
audio_cache/response_xxxxxx.mp3并自动播放。 - 一次对话结束,进入下一次录音等待。
至此,一个完整的、免费的、可本地运行的 TTS+STT+LLM 三合一系统就成功运行起来了。
6. 常见问题与排查思路
在实际部署和运行中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
运行python main.py报ModuleNotFoundError | 依赖未安装或虚拟环境未激活。 | 1. 确认终端路径在项目根目录。 2. 执行 pip list检查vosk,langchain,edge-tts等包是否存在。3. 若缺失,重新运行 pip install -r requirements.txt。 |
| Vosk 识别不出任何语音 | 1. 麦克风未正确连接或未被授权。 2. 音频采样率不匹配。 3. 模型语言与输入语音不匹配。 | 1. 检查系统音频设置,确保麦克风是默认输入设备。 2. 尝试使用 sounddevice查询设备:python -c "import sounddevice; print(sounddevice.query_devices())"。3. 确认下载的Vosk模型语言(如英文)与你说话的语种一致。 4. 尝试在安静环境下清晰、缓慢地说话。 |
Ollama 连接错误 (Connection refused) | Ollama 服务未启动。 | 1. 新开一个终端,运行ollama serve并保持运行。2. 在主终端中,运行 curl http://localhost:11434/api/tags测试API是否可达。3. 确认 llm_module.py中的base_url与 Ollama 服务地址一致。 |
| Edge-TTS 报错或没有声音 | 1. 网络问题,无法连接微软服务。 2. 指定的 voice不存在或不可用。3. 系统缺少MP3播放器。 | 1. 检查网络连接。 2. 运行 edge-tts --list-voices查看所有可用语音,更换一个语音名称(如zh-CN-XiaoxiaoNeural)。3. 对于播放问题,可以优先使用 save_to_file功能保存文件,然后用本地播放器打开确认。 |
| 程序运行缓慢,LLM响应慢 | 本地LLM推理需要消耗大量CPU/GPU资源。 | 1. 确认电脑配置,尤其是内存是否足够(7B模型通常需要8GB以上内存)。 2. 考虑使用更小的模型(如 tinyllama)。3. 在 ollama run时添加--num-gpu参数利用GPU加速(如有NVIDIA显卡)。4. 这只是演示流程,生产环境可考虑优化或使用API。 |
| 录音时间固定为5秒,不灵活 | 代码中listen_and_transcribe(duration=5)写死了时长。 | 修改stt_module.py中的逻辑,例如改为检测静音端点(VSR)或通过按键控制录音开始/结束。这需要更复杂的音频处理逻辑。 |
7. 最佳实践与工程建议
将本方案用于实际项目时,需要考虑以下方面以提升稳定性、性能和用户体验。
7.1 配置化管理
将模型路径、语音名称、Ollama地址、录音参数等硬编码信息提取到配置文件(如config.yaml或.env文件)中。
# config.yaml stt: model_path: "./models/vosk-model-small-cn-0.22" # 切换为中文模型 sample_rate: 16000 llm: model_name: "mistral:7b" base_url: "http://localhost:11434" timeout: 30 tts: use_edge_tts: true voice: "zh-CN-XiaoxiaoNeural" # 中文语音 offline_engine_rate: 150 app: record_duration: 7 exit_keywords: ["退出", "quit", "exit"]在主程序中用PyYAML库读取配置。
7.2 异步处理与性能优化
当前的流程是同步的:录音 -> 识别 -> LLM生成 -> TTS合成。其中LLM生成可能耗时数秒到数十秒,会阻塞整个程序。
- 优化建议:引入异步编程(
asyncio)或消息队列。将LLM调用放入独立线程或进程,主线程在等待时可以提供“正在思考”的语音或视觉反馈。 - 流式响应:对于LLM,可以探索Ollama的流式输出API,实现逐词生成和TTS,减少用户等待的“空白期”。
7.3 错误处理与降级策略
网络或服务不稳定是常态。
- TTS降级:如
tts_module.py所示,当Edge-TTS失败时,自动切换到本地的pyttsx3。 - LLM降级:可以配置一个备用的LLM API(如开源的OpenAI兼容API),当本地Ollama不可用时切换。
- STT降级:Vosk离线识别是本地保障。也可以集成一个在线的STT服务(如
SpeechRecognition库调用Google Web Speech API)作为备选,但需注意隐私和网络。
7.4 音频处理增强
- 噪音抑制:在录音回调中引入简单的噪音抑制算法或使用如
noisereduce库,提升Vosk在嘈杂环境下的识别率。 - 静音检测:实现基于能量的静音检测(VAD),让用户自由控制说话时长,而不是固定录音。
- 音频格式转换:确保传递给Vosk的音频格式(采样率、位深、声道)完全符合模型要求。
7.5 安全与隐私
- 本地化优先:本方案的核心优势是数据不出本地。确保Vosk模型、Ollama模型均从官方渠道下载,并部署在可信环境中。
- 敏感信息过滤:在将STT识别出的文本发送给LLM前,可增加一个过滤层,移除或脱敏可能的个人身份信息(PII)。
- 对话历史管理:如果LLM需要上下文记忆,避免在内存中长期存储明文对话记录。可考虑短期缓存或用户授权后的加密存储。
7.6 扩展方向
- 集成RAG:使用
LangChain的RetrievalQA链,将本地文档库接入LLM,让助手能够回答特定领域知识。 - 多模态:结合视觉模型,升级为“看、听、想、说”的多模态系统。
- Web服务化:使用
FastAPI或Flask将核心功能封装成HTTP API,供Web或移动前端调用。 - 唤醒词:集成像
Porcupine这样的离线唤醒词引擎,实现“Hey Assistant”式的触发。
这套整合方案提供了一个坚实的起点。它验证了利用免费开源工具构建完整语音AI交互流程的可行性。你可以根据项目需求,对每个模块进行深度定制和替换,例如使用更精确的Whisper模型做STT,更换为ChatGLM等中文LLM,或接入更专业的TTS服务。关键在于理解数据流(音频->文本->智能文本->音频)和模块间的接口设计,剩下的便是针对具体场景的优化和打磨了。