🚀 前言:为什么要搞“纯离线”?

最近入手了 树莓派 5 (8GB),性能确实强悍。作为一名极客,我一直想给家里装个“贾维斯”。但市面上的方案或者之前的尝试,往往存在两个痛点:

  1. 延迟高:说句话要等云端 API 转圈圈,体验很差。

  2. 依赖网络:断网就变“人工智障”,而且 Google/OpenAI 的 API 在国内网络环境下并不稳定。

既然树莓派 5 性能这么强,能不能做一个完全断网、毫秒级响应、还能控制硬件的“真·离线”语音管家?

经过几天的折腾(和无数次报错),我终于搞定了!这套方案集成了 Ollama (Qwen 2.5) 做大脑,Vosk 做离线听觉,pyttsx3 做离线发声,还能通过 GPIO 控制家里的灯和风扇。

今天就把这套全链路离线 AIoT 方案分享给大家,顺便记录一下那些让我头秃的坑。

🛠️ 硬件清单

  • 主板:Raspberry Pi 5 (8GB) —— 跑大模型内存越大越好,4GB 勉强,8GB 顺滑。

  • 听觉:USB 全向麦克风。

  • 发声:USB 迷你音箱。

    • 注意:树莓派 5 取消了 3.5mm 耳机孔,必须用 USB 声卡或 HDMI 音频!这点坑了我很久。

  • 执行:LED 灯珠 (GPIO 17) + 散热风扇 (GPIO 27)。

🧠 技术架构:如何实现毫秒级响应?

为了追求极致速度和隐私安全,我放弃了庞大的 SpeechRecognition + 在线 API 方案,转而采用全离线架构:

  1. 耳朵 (STT):使用 Vosk 离线模型。40MB 的轻量级中文模型 (vosk-model-small-cn),在 Pi 5 上识别几乎是瞬时的。

  2. 大脑 (LLM):部署 Ollama 运行 Qwen2.5:1.5b。15亿参数的模型在 Pi 5 上推理速度极快,完全满足指令理解需求。

  3. 嘴巴 (TTS):使用 pyttsx3 + espeak。虽然声音略带机械感,但胜在 0 延迟,不需要等待云端生成。

  4. 兜底机制 (核心亮点):为了防止小模型偶尔“抽风”(不返回 JSON 指令),我加入了一层规则引擎兜底,确保控制指令 100% 执行。

💣 踩坑实录(避坑必读!)

这一路走来全是坑,希望能帮大家避雷:

🕳️ 坑一:TTS 只有电流声或报错 Error 524

现象:代码运行正常,日志显示 Jarvis: 系统上线,但音箱没声音,或者报错 Unknown error 524 / Channels count non available原因

  1. 树莓派 5 音频架构变了(PipeWire/ALSA)。

  2. TTS 生成的是单声道音频,而我的 USB 声卡只支持立体声,底层驱动直接拒收。

  3. 录音和播放同时抢占声卡资源。

✅ 解决: 配置 ~/.asoundrc 文件,使用 plug 插件自动转换格式,并强制绑定 USB 声卡 (Card 2)。

# nano ~/.asoundrc
pcm.!default {
    type plug
    slave {
        pcm "hw:2,0"  # 2是我的USB声卡编号,用 aplay -l 查看
    }
}
ctl.!default {
    type hw
    card 2
}

🕳️ 坑二:贾维斯说“鸟语” (Chinese lite?)

现象:明明给他发的是中文文本,它读出来却是奇怪的英文发音或者乱码。 原因pyttsx3 在 Linux 下默认调用 espeak 的英文引擎,不会自动切换到中文模式。 ✅ 解决:在代码初始化时,强制遍历并锁定中文语音包。

# 自动寻找中文语音包逻辑
voices = engine.getProperty('voices')
for v in voices:
    if 'zh' in v.id or 'chinese' in v.name.lower():
        engine.setProperty('voice', v.id)
        break

🕳️ 坑三:大模型“偷懒”

现象:Vosk 听到了“把灯打开”,但 Qwen 1.5B 有时候沉迷聊天,返回的 JSON 里 devicenull,只陪聊不干活。 ✅ 解决:增加混合逻辑 (Hybrid Logic)。 如果 AI 返回空指令,代码会自动正则匹配关键词(“开灯”、“亮”)。AI 负责聊天和复杂理解,规则负责保命兜底

💻 核心代码 (Jarvis.py)

这是最终版的完整代码,集成了 Vosk 听觉 + Ollama 大脑 + TTS 表达 + GPIO 控制 + 规则兜底

环境准备

# 1. 安装系统依赖
sudo apt install espeak espeak-ng python3-pyaudio libasound2-dev

# 2. 安装 Python 库
pip install vosk pyaudio pyttsx3 requests RPi.GPIO

# 3. 下载模型
# 请去 Vosk 官网下载 vosk-model-small-cn-0.22 并解压为 'model' 文件夹

完整 Python 代码

"""
项目名称: Raspberry Pi 5 AIoT 离线语音管家 (Jarvis)
硬件平台: Raspberry Pi 5 (8GB)
核心技术栈:
  - 听觉 (STT): Vosk (离线模型: vosk-model-small-cn-0.22)
  - 大脑 (LLM): Ollama + Qwen2.5:1.5b (本地运行)
  - 视觉 (TTS): pyttsx3 + espeak (离线语音合成)
  - 控制 (GPIO): RPi.GPIO
作者: 新
日期: 2025
"""

# --- 1. 硬件配置与初始化 ---
PIN_LIGHT = 17  # 灯光 GPIO
PIN_FAN = 27    # 风扇 GPIO

# 强制清理 GPIO 状态,防止被旧进程占用
try:
    GPIO.setmode(GPIO.BCM)
    GPIO.cleanup()
except:
    pass

try:
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(PIN_LIGHT, GPIO.OUT, initial=GPIO.LOW)
    GPIO.setup(PIN_FAN, GPIO.OUT, initial=GPIO.LOW)
    IS_PI = True
    print(f"✅ 硬件就绪!控制引脚: 灯={PIN_LIGHT}, 风扇={PIN_FAN}")
    
    # --- 💡 开机自检 (视觉反馈) ---
    print("👀 硬件自检中 (灯光闪烁)...")
    for _ in range(2):
        GPIO.output(PIN_LIGHT, GPIO.HIGH)
        time.sleep(0.3)
        GPIO.output(PIN_LIGHT, GPIO.LOW)
        time.sleep(0.3)
    print("✅ 自检完成。")

except Exception as e:
    IS_PI = False
    print(f"⚠️ 进入模拟模式 (GPIO 错误): {e}")

# --- 2. 离线语音合成 (TTS) ---
engine = pyttsx3.init()

# 自动寻找并切换中文语音包
voices = engine.getProperty('voices')
found_zh = False
for v in voices:
    if 'zh' in v.id or 'chinese' in v.name.lower():
        engine.setProperty('voice', v.id)
        found_zh = True
        print(f"✅ TTS 已切换中文: {v.id}")
        break

if not found_zh:
    try:
        engine.setProperty('voice', 'zh') # 强制尝试
    except:
        pass

engine.setProperty('rate', 165)  # 语速
engine.setProperty('volume', 1.0) # 音量

def speak(text):
    """ 文字转语音输出 """
    print(f"🤖 Jarvis: {text}")
    try:
        engine.say(text)
        engine.runAndWait()
    except:
        pass

# --- 3. 离线语音识别 (Vosk) ---
if not os.path.exists("model"):
    print("❌ 错误:找不到 'model' 文件夹,请下载 Vosk 中文模型。")
    exit(1)

# 屏蔽底层 ALSA 音频驱动的冗余报错
try:
    from ctypes import *
    ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
    def py_error_handler(filename, line, function, err, fmt): pass
    c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
    asound = cdll.LoadLibrary('libasound.so')
    asound.snd_lib_error_set_handler(c_error_handler)
except:
    pass

print("⏳ 正在加载 Vosk 离线模型...")
model = Model("model")
rec = KaldiRecognizer(model, 16000)
p = pyaudio.PyAudio()

# 打开麦克风流 (采样率 16000)
stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=4000)
stream.start_stream()

# --- 4. AI 大脑处理 (Ollama + 规则兜底) ---
def ask_ai(text):
    url = "http://127.0.0.1:11434/api/chat"
    
    # System Prompt: 定义 AI 的人设和输出格式
    system_prompt = """
    你是一个智能管家。
    任务:判断用户意图,返回 JSON。
    规则:
    1. 如果用户想控制设备,必须设置 "device" (light/fan) 和 "action" (on/off)。
    2. 如果只是闲聊,"device" 设为 null。
    
    示例:
    用户:把灯打开 -> {"device": "light", "action": "on", "reply": "好的,灯亮了"}
    用户:你好 -> {"device": null, "action": null, "reply": "你好呀!"}
    """
    
    ai_data = {"device": None, "action": None, "reply": "我没听清"}
    
    # 1. 尝试请求本地 LLM
    try:
        res = requests.post(url, json={
            "model": "qwen2.5:1.5b",
            "messages": [
                {'role': 'system', 'content': system_prompt},
                {'role': 'user', 'content': text}
            ],
            "stream": False
        }, timeout=10)
        
        raw = res.json()['message']['content']
        # 清洗数据,提取 JSON
        clean = raw.replace("```json", "").replace("```", "").strip()
        import re
        match = re.search(r'\{.*\}', clean, re.DOTALL)
        if match:
            ai_data = json.loads(match.group())
        else:
            ai_data = {"reply": clean, "device": None, "action": None}
            
    except:
        return {"reply": "Ollama 服务未启动", "device": None}

    # 🔥 2. 规则兜底 (Rule-based Fallback) 🔥
    # 如果小模型“漏抓”了指令,使用关键词强制修正,确保控制成功率 100%
    if ai_data.get("device") is None:
        text_lower = text.lower()
        # print("⚠️ 检测到 AI 未识别设备,启动规则兜底检查...")
        
        if "灯" in text_lower:
            if any(x in text_lower for x in ["开", "亮", "open", "on"]):
                ai_data.update({"device": "light", "action": "on", "reply": "好的,灯已开启 (兜底)"})
            elif any(x in text_lower for x in ["关", "灭", "close", "off"]):
                ai_data.update({"device": "light", "action": "off", "reply": "好的,灯已关闭 (兜底)"})
                
        elif "风扇" in text_lower:
            if any(x in text_lower for x in ["开", "转", "open", "on"]):
                ai_data.update({"device": "fan", "action": "on", "reply": "风扇启动 (兜底)"})
            elif any(x in text_lower for x in ["关", "停", "close", "off"]):
                ai_data.update({"device": "fan", "action": "off", "reply": "风扇停止 (兜底)"})

    return ai_data

# --- 5. 主循环 ---
print("\n✅ 系统就绪!请说话...")
speak("系统启动完毕")

try:
    while True:
        data = stream.read(4000, exception_on_overflow=False)
        if len(data) == 0: break

        # Vosk 实时监听
        if rec.AcceptWaveform(data):
            result = json.loads(rec.Result())
            text = result['text'].replace(" ", "")
            
            if text:
                print(f"👂 听到: {text}")
                # 关键词唤醒过滤,防止杂音误触
                keywords = ["灯", "风扇", "打开", "关", "你好", "是谁", "笑话", "天气", "贾维斯"]
                
                if any(k in text for k in keywords):
                    action_data = ask_ai(text)
                    print(f"🧠 AI 最终决策: {action_data}") 
                    
                    device = action_data.get('device')
                    action = action_data.get('action')
                    reply = action_data.get('reply', '好的')
                    
                    # 硬件执行逻辑 (兼容大小写)
                    if device and device.lower() == 'light' and IS_PI:
                        state = GPIO.HIGH if action == 'on' else GPIO.LOW
                        GPIO.output(PIN_LIGHT, state)
                        print(f"💡 [硬件操作] 灯 -> {action}")
                        
                    elif device and device.lower() == 'fan' and IS_PI:
                        state = GPIO.HIGH if action == 'on' else GPIO.LOW
                        GPIO.output(PIN_FAN, state)
                        print(f"💨 [硬件操作] 风扇 -> {action}")
                    
                    speak(reply)

except KeyboardInterrupt:
    print("\n退出系统")
    stream.stop_stream()
    stream.close()
    p.terminate()
    if IS_PI: GPIO.cleanup()

🎉 最终效果

现在,不管是喊“打开风扇”,还是跟它闲聊“讲个笑话”,树莓派都能在 1秒左右 做出反应。

  • 闲聊模式:幽默风趣。

  • 指令模式:指哪打哪,灯光闪烁瞬间响应。

最重要的是:这一切都不需要联网! 即使拔掉网线,它依然能稳定运行。

代码详情:Pi-AIoT-Central

🔮 下一步预告

耳朵(语音)和嘴巴(TTS)都有了,下一步就是**“眼睛”! 下一篇文章,我将手把手教大家如何在树莓派 5 上部署 YOLOv8,结合 NPU 跑满 60fps,实现“人来灯亮,人走灯灭”**的视觉主动智能!

感兴趣的朋友点个关注,我们下期见!👋

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐