語音助手Antenna(長期更新)

JiamingHu121發表於2024-10-03

【目標】
製作一個個人的語音助手隨時能聊天,核心要求是能夠了解我,包括能夠認識我周圍的人,知曉我的生活經歷,同時也能夠連線網路搜尋內容,為我提供知識。強調一個私人性,得知道我的個人資訊並能聊起來。按照現在大模型的發展速度,肯定已經、或即將有這種東西了,但是一是不想付費,二是自己搞的過程中也學學東西。害本來是玩嵌入式的,現在演化到這個專案了。這個助手的名字就叫Antenna好了,原意“天線”,這裡致敬/來源於Halo裡的Cortana。

【實現思路】
該助手需要長期開機收集我的個人生活資訊,目前主要是語音內容,有針對性的定期finetune。因此軟體層面,需要基於一個預訓練的大語言模型進行語言互動,一個語音識別模型將語音轉換為文字;硬體層面需要錄音和相應的控制裝置、大模型的部署裝置和與大模型通訊的裝置。

【基本方案】

飯得一口一口吃,初步設想先不動硬體,軟體包括雲和本地兩個部分。使用本地裝置收集我的語音內容,整理成適配大模型的資料集格式,定期上傳雲平臺進行大模型finetune,然後定期下載finetune後的模型至本地進行merge;本地執行大模型平時互動的inference。

我手裡算力最強、同時也是用的最多的本地裝置是一臺2021年的MacBook Pro,搭載Apple M1 Pro晶片組,8+2核CPU,16G記憶體,16核GPU,驅動架構為蘋果自己的Metal 3,目前系統為Sequoia 15.0. 另有一臺實驗室的Intel桌上型電腦,無GPU,唯一優點是windows系統,且儲存幾個T...

嵌入式裝置方面,算力最強的是意法半導體的STM32MP157,目前只是一塊來自正點原子的核心板,主頻800MHz,內建GPU(但不會用),板載1G記憶體和8G儲存eMMC和千兆PHY,感覺有可能帶的動簡單的模型。。

另看上了基於RK3566的泰山派卡片電腦,主頻1.8GHz,搭載NPU和支援OpenGL的GPU,2G記憶體,價格也挺友好,考慮入手。

【測試-學習大模型使用】

kimi廣告做的多,用起來也不錯,而且免費,API也很完善,但是不開源沒法finetune。但是先做點能跑的玩意兒出來鼓舞下士氣還是很有必要滴。先用python基於kimi做了一個僅inference的語音對話demo在Mac上跑著玩一下,主要包括三部分:(1)呼叫Mac麥克風錄音後轉文字;(2)文字輸入給kimi並獲得回答文字;(3)文字轉語音播放。步驟(1)最廣泛的包是線上識別的GoogleSpeech,免費、準確度高、中英通吃,但是得聯網,有時候略慢;另一個不聯網方案是vosk,不設計特殊名詞和英文時準確度不錯,且本地跑速度很快,調包大法用起來都很簡單,我都是直接用kimi生成的code改一改就用了。步驟(2)用的kimi的API,教程很詳細,且內建了網路搜尋功能,很nice,直接copy過來。步驟(3)用的pyttsx3,中文語音只有Tingting,很不好聽,不過用起來倒也簡單,速度快且完全本地,先湊合著。程式碼全文如下:

點選檢視程式碼
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Sep 28 19:13:28 2024

@author: jiaminghu
"""

from openai import OpenAI
import cv2
import sounddevice as sd
import numpy as np
from scipy.io.wavfile import write

import pyttsx3
import pyaudio
from vosk import Model, KaldiRecognizer
import json
import time

from typing import *
from openai.types.chat.chat_completion import Choice
import speech_recognition as sr

def listen_for_command():
    # 初始化識別器
    recognizer = sr.Recognizer()

    # 使用麥克風作為音訊源
    with sr.Microphone() as source:
        print("Please speak command...")
        audio = recognizer.listen(source)

        try:
            # 使用Google的免費Web API進行語音識別
            command = recognizer.recognize_google(audio, language='zh-CN') #en-US'
            print("You said: " + command)
            
            # 在這裡,你可以將command傳送給Kimi搜尋
            answer = kimi_search(command)
            
            # 將文字轉換為語音並播放
            engine.say(answer)
            engine.runAndWait()

            # 在這裡,你可以將command傳送給Kimi搜尋
            # 例如,你可以呼叫一個函式來處理搜尋請求
            return command

        except sr.UnknownValueError:
            print("Google Speech Recognition could not understand audio")
            return None
        except sr.RequestError as e:
            print("Could not request results from Google Speech Recognition service; {0}".format(e))
            return None
        
        

def listen_for_command_localvosk():
    
    # 指定模型路徑
    model_path = "./vosk-model-small-cn-0.22"
    model = Model(model_path)
    
    # 初始化識別器,取樣率為16000Hz
    recognizer = KaldiRecognizer(model, 16000)
    
    # 定義音訊流引數
    chunk = 1024
    format = pyaudio.paInt16
    channels = 1
    rate = 16000
    p = pyaudio.PyAudio()
    
    # 開啟音訊流
    stream = p.open(format=format, channels=channels, rate=rate, input=True, frames_per_buffer=chunk)
    
    print("開始說話,說'停止'以結束錄音...")
    
    # 記錄開始時間
    start_time = time.time()
    
    text = ''
    # 讀取音訊流
    while True:
        data = stream.read(chunk)
        if recognizer.AcceptWaveform(data):
            result = recognizer.Result()
            if result:
                result_json = json.loads(result)
                if 'text' in result_json:
                    if '停止' in result_json['text']:
                        print("停止錄音")
                        break
                    print("識別結果:", result_json['text'])
                    text += result_json['text']
                    # 檢查是否說出了“停止”
                    
        if time.time() - start_time > 20:  # 設定超時時間為10秒,也可以根據需要調整
            print("超時,自動停止錄音")
            break
    
    # 關閉音訊流
    stream.stop_stream()
    stream.close()
    p.terminate()
    
    command = text.strip()
    print("You said: " + command)
    
    # 在這裡,你可以將command傳送給Kimi搜尋
    answer = kimi_search(command)
    
    # 將文字轉換為語音並播放
    engine.say(answer)
    engine.runAndWait()

    return command



def kimi_search_offline(query):
    
    # 這裡應該是呼叫Kimi搜尋的程式碼
    query = '你好kimi, ' + query
    print("Searching Kimi for: " + query)
    client = OpenAI(
        api_key = "API_KEY", #得用自己的KEY哦
        base_url = "https://api.moonshot.cn/v1",
    )
     
    completion = client.chat.completions.create(
        model = "moonshot-v1-8k",
        messages = [
            {"role": "system", "content": system_query},
            {"role": "user", "content": query}
        ],
        temperature = 0.8,
    )
    
    print(completion.choices[0].message.content)
   

    # 要轉換為語音的文字
    return completion.choices[0].message.content
    

    
def kimi_search(query):
    query = '你好kimi, ' + query
    print("Searching Kimi for: " + query)
    client = OpenAI(
        api_key = "sk-NZ9KYlSLrfCUEe3gvGMO6YSP99Lf7n5OVzoNer6xRtfHfbti",
        base_url = "https://api.moonshot.cn/v1",
    )
    
    messages = [
        {"role": "system", "content": system_query},
    ]
     
    # 初始提問
    messages.append({
        "role": "user",
        "content": query
    })
    

 
    # search 工具的具體實現,這裡我們只需要返回引數即可
    def search_impl(arguments: Dict[str, Any]) -> Any:
        """
        在使用 Moonshot AI 提供的 search 工具的場合,只需要原封不動返回 arguments 即可,
        不需要額外的處理邏輯。
     
        但如果你想使用其他模型,並保留聯網搜尋的功能,那你只需要修改這裡的實現(例如呼叫搜尋
        和獲取網頁內容等),函式簽名不變,依然是 work 的。
     
        這最大程度保證了相容性,允許你在不同的模型間切換,並且不需要對程式碼有破壞性的修改。
        """
        return arguments
     
     
    def chat(messages) -> Choice:
        completion = client.chat.completions.create(
            model="moonshot-v1-128k",
            messages=messages,
            temperature=0.3,
            tools=[
                {
                    "type": "builtin_function",  # <-- 使用 builtin_function 宣告 $web_search 函式,請在每次請求都完整地帶上 tools 宣告
                    "function": {
                        "name": "$web_search",
                    },
                }
            ]
        )
        return completion.choices[0]
     
    finish_reason = None
    while finish_reason is None or finish_reason == "tool_calls":
        choice = chat(messages)
        finish_reason = choice.finish_reason
        if finish_reason == "tool_calls":  # <-- 判斷當前返回內容是否包含 tool_calls
            messages.append(choice.message)  # <-- 我們將 Kimi 大模型返回給我們的 assistant 訊息也新增到上下文中,以便於下次請求時 Kimi 大模型能理解我們的訴求
            for tool_call in choice.message.tool_calls:  # <-- tool_calls 可能是多個,因此我們使用迴圈逐個執行
                tool_call_name = tool_call.function.name
                tool_call_arguments = json.loads(tool_call.function.arguments)  # <-- arguments 是序列化後的 JSON Object,我們需要使用 json.loads 反序列化一下
                if tool_call_name == "$web_search":
                    tool_result = search_impl(tool_call_arguments)
                else:
                    tool_result = f"Error: unable to find tool by name '{tool_call_name}'"
     
                # 使用函式執行結果構造一個 role=tool 的 message,以此來向模型展示工具呼叫的結果;
                # 注意,我們需要在 message 中提供 tool_call_id 和 name 欄位,以便 Kimi 大模型
                # 能正確匹配到對應的 tool_call。
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": tool_call_name,
                    "content": json.dumps(tool_result),  # <-- 我們約定使用字串格式向 Kimi 大模型提交工具呼叫結果,因此在這裡使用 json.dumps 將執行結果序列化成字串
                })
    
    print(choice.message.content)
    
    return choice.message.content  # <-- 在這裡,我們才將模型生成的回覆返回給使用者

    

if __name__ == "__main__":
    system_query = "你是 Kimi,由 Moonshot AI 提供的人工智慧助手,你更擅長中文和英文的對話。你之後收到的文字來源於語音識別,所以可能會有誤差、重複和標點不全的情況,請按照一個人通常說話的習慣進行理解。"
    engine = pyttsx3.init()
    engine.setProperty('voice', 'com.apple.voice.compact.zh-CN.Tingting')
    engine.setProperty('rate', 220)
    
    command = listen_for_command()
    #command = listen_for_command_localvosk()

玩夠了以後,繼續測試大模型的finetine。優先考慮了阿里雲的通義千問系列QWen,因為中英都支援的好,而且是開源裡目前效能最好的嘛。inference很順利,但是官方的finetune script不支援蘋果的GPU架構Metal Performance Shaders(MPS),這讓我手裡唯一的GPU都用不起來了。好在MPS的生態做的不錯,有一個專門的repo叫MLX,介紹說it is an array framework for machine learning research on Apple silicon(https://github.com/ml-explore) ,特別有llm的例子,就跟著一篇部落格(https://apeatling.com/articles/part-3-fine-tuning-your-llm-using-the-mlx-framework/) 使用測試一下Mistral-7B-Instruct-v0.2,然後看看能不能搞QWen。

另一方面同步測試的是語音,一直用Tingting肯定也受不了。這個平臺(https://fish.audio/zh-CN) 簡單用了一下搞的不錯,上傳了物件的一段20s音訊,很快就能學到語音模型,現在研究一下API和價格。

相關文章