引言
今年上半年,我曾在基于RK3566的嵌入式開發(fā)板上本地部署并運行大模型,當(dāng)時僅能在終端界面使用文字進(jìn)行交互,而我的進(jìn)一步目標(biāo)是實現(xiàn)本地的大模型語音交互。但限于我手里的開發(fā)板內(nèi)存不足,無法加載本地的語音識別模型,同時經(jīng)費有限,只能暫時擱置。下半年,遇到電源網(wǎng)和得捷舉辦DIY活動,提供600元的經(jīng)費報銷,使得該項目得以繼續(xù)。
本文將介紹在嵌入式終端上,基于本地大模型實現(xiàn)的多語言離線語音聊天機(jī)器人。其中,語音識別、大模型推理、文字轉(zhuǎn)語音(TTS)都是在嵌入式終端基于本地模型實現(xiàn)的,無需接入互聯(lián)網(wǎng)環(huán)境。
特點
-基于嵌入式終端實現(xiàn)
-無需聯(lián)網(wǎng),完全離線運行
-多語言支持(中、英)
-代碼開源,模型易部署及更換
硬件環(huán)境
-Raspberry Pi 5(quad-core Arm Cortex A76 processor @ 2.4GHz, 8GB RAM)
-WM8960 音頻模塊
-2寸LCD主屏+ 0.96寸OLED雙副屏
軟件及模型
-Python: 3.11.2
-推理框架:llama-cpp-python
-語音識別:SenseVoice大模型
-推理模型:千問大模型
-文本轉(zhuǎn)語音:Piper TTS
整體框架
(按要求使用Scheme-it繪制)
環(huán)境準(zhǔn)備
安裝llama推理框架
直接用pip安裝:
pip install llama-cpp-python
安裝SenseVoice依賴
在SenseVoice的Github倉庫,提供了requirements.txt。如果是直接使用我提供的源碼,無需拉取SenseVoice倉庫,requirements.txt存放于\models\SenseVoiceSmall
目錄。使用pip安裝必要的依賴:
pip install -r requirements.txt
安裝Piper TTS
Piper TTS是我目前找到的較優(yōu)離線TTS,語音接近人聲,加載速度快,完全離線運行。它無需特別安裝,只需要下載編譯好的二進(jìn)制可執(zhí)行文件,即可使用,我提供的源碼已經(jīng)直接包含,存放于\piper
目錄。
特別說明:上述安裝截圖中pip安裝含有
--break-system-packages
選項,這是我的系統(tǒng)Python結(jié)構(gòu)的原因,在其它系統(tǒng)Python環(huán)境下,可能是不需要的。
安裝音頻及顯示驅(qū)動
本文使用的音頻及顯示模塊,均來自微雪電子,可直接參考對應(yīng)模塊的教程,進(jìn)行驅(qū)動安裝即可,如有問題,可聯(lián)系其技術(shù)支持。
-WM8960音頻模塊驅(qū)動安裝
-LCD+OLED三聯(lián)屏顯示驅(qū)動安裝
獲取本項目源碼
-獲取項目源碼:
本項目完整源碼,通過gitee開源,可通過git拉取
-放置模型文件:
受限于git倉庫對單個文件的大小的限制,兩個較大的模型文件單獨提供網(wǎng)盤下載
注:源碼網(wǎng)址見文末
代碼說明
線程結(jié)構(gòu)
本項目代碼主要由多個線程組成,包含按鍵線程、錄音線程、語音識別線程、模型推理線程、文字轉(zhuǎn)語音線程、顯示線程等,各線程通過事件進(jìn)行觸發(fā)并流轉(zhuǎn)運作。
主要線程代碼
Key線程
Device.pin_factory = LGPIOFactory()
# key init
key2 = Button(17)
def key2_pressed():
start_record_event.set()
stop_tts_event.set()
show_record_event.set()
def key2_released():
stop_record_event.set()
model_doing_event.set()
stop_tts_event.clear()
show_record_event.clear()
# Bind key press event
key2.when_pressed = key2_pressed
key2.when_released = key2_released
該線程主要監(jiān)聽按鍵的按下和釋放事件,以觸發(fā)語音錄制及識別等相關(guān)動作。
錄音線程
def recording_thread():
while True:
start_record_event.wait()
start_record_event.clear()
device = "default"
wavfile = wave.open(f"{current_dir}/record.wav", "wb")
mic = alsaaudio.PCM(
alsaaudio.PCM_CAPTURE,
alsaaudio.PCM_NONBLOCK,
channels=1,
rate=44100,
format=alsaaudio.PCM_FORMAT_S16_LE,
periodsize=160,
device=device,
)
wavfile.setnchannels(1)
wavfile.setsampwidth(2) # PCM_FORMAT_S16_LE
wavfile.setframerate(44100)
print("Start speaking...")
time_start = datetime.now()
while True:
if stop_record_event.is_set():
stop_record_event.clear()
time_stop = datetime.now()
print("Stop speaking...")
wavfile.close()
if time_stop.timestamp() - time_start.timestamp() >= 1:
trig_sensevoice_event.set()
else:
print("The speaking time is too short")
model_doing_event.clear()
break
# Read data from device
l, data = mic.read()
if l:
wavfile.writeframes(data)
time.sleep(0.001)
錄音線程,在KEY按下后被觸發(fā)執(zhí)行循環(huán)錄制,KEY釋放后退出錄制。此處還做了簡單的錄音時長的判斷,因為當(dāng)錄音時長過短時,后續(xù)的語音識別可能會報錯。
語音識別線程
def sensevoice_thread():
from model import SenseVoiceSmall
from funasr.utils.postprocess_utils import rich_transcription_postprocess
model_dir = f"{current_dir}/models/SenseVoiceSmall"
m, kwargs = SenseVoiceSmall.from_pretrained(model=model_dir, device="cuda:0")
m.eval()
senvc_load_done.set()
print("Load sensevoice model done")
while True:
trig_sensevoice_event.wait()
trig_sensevoice_event.clear()
res = m.inference(
data_in=f"{current_dir}/record.wav",
language="auto", # "zh", "en", "yue", "ja", "ko", "nospeech"
use_itn=False,
ban_emo_unk=False,
**kwargs,
)
text = rich_transcription_postprocess(res[0][0]["text"])
ask_text_q.put(text)
trig_llama_event.set()
在錄音線程正常執(zhí)行完成后,觸發(fā)執(zhí)行語音識別線程。SenseVoice語音識別使用較為簡單,可自動識別多種語言,生成的文本直接放置到消息隊列中,供下一步模型推理使用。模塊的初始import是較費時間的,為了不影響程序的整體加載時間,所以關(guān)于SenseVoice模塊的import處理也放置在了線程中,而不是統(tǒng)一放在文件的開頭
------無奈的結(jié)束分割線(受論壇帖子字?jǐn)?shù)限制,后續(xù)見下一帖子)-----