//www.yanusi.cn/bbs/2778146.html
------無奈的起始分割線(受論壇帖子字數限制,前文見上一帖子)-----
主要線程代碼
模型推理線程
def llama_thread():
global bool_Chinese_tts
model = llama_cpp.Llama(
model_path=f"{current_dir}/models/llama/qwen1_5-0_5b-chat-q4_0.gguf",
# n_ctx = 4096,
verbose=False,
)
ch_punctuations_re = "[,。?;]"
llama_load_done.set()
messages_history = []
max_msg_history = 2
print("Load llama model done")
while True:
trig_llama_event.wait()
trig_llama_event.clear()
ask_text = ask_text_q.get()
print(ask_text)
messages_history.append({"role": "user", "content": ask_text})
if len(messages_history) > max_msg_history:
messages_history = messages_history[-max_msg_history:]
ans_text = model.create_chat_completion(
messages=messages_history,
logprobs=False,
# stream=True,
repeat_penalty = 1.2,
max_tokens=100,
)
ans_text = ans_text["choices"][0]["message"]["content"]
messages_history.append({"role": "assistant", "content": ans_text})
if len(messages_history) > max_msg_history:
messages_history = messages_history[-max_msg_history:]
print(ans_text)
ans_text_tts = ans_text.replace(",", "。")
bool_Chinese_tts = bool(re.search(r"[\u4e00-\u9fff]", ans_text_tts)) # Chinese?
ans_text_q.put(ans_text_tts)
model_doing_event.clear()
模型推理部分,做了一些特殊處理:
-模型傳入的輸入信息,是包含前一次模型的輸入及輸出的,以便讓模型每次推理具有一定的上下文信息。但增加上下文長度,會犧牲模型的推理速度,所以應該根據實際算力情況,合理規劃上下文長度。
-模型做了一些參數設置,例如限制了最大輸出Tokens數,以及重復性懲罰repeat_penalty等,避免模型一次輸出太多信息,甚至重復輸出一些無效信息。
-模式輸出文本做了處理,將中文逗號全部替換成中文句號,這主要是簡單解決Piper TTS對中文逗號幾乎沒有語音停頓的局限性。
-判斷模型輸出的語言類型,以便讓Piper TTS對應加載不同的語言模型。
注:
-如果需要更換模型,只需要簡單修改代碼中model_path指定到對應的gguf文件即可
-由于增加了1次對話上下文信息,所以一定程度犧牲了模型推理速度
-考慮后續TTS輸出的連貫性,模型未采用流式輸出,所以感官上從模型輸入到完整輸出,推理會有較長的等待時間
文本轉語音線程
def tts_thread():
global bool_Chinese_tts
piper_cmd_zh = f"{current_dir}/piper/piper --model {current_dir}/models/piper/zh_CN-huayan-medium.onnx --output-raw | aplay -r 22050 -f S16_LE -t raw -"
piper_cmd_en = f"{current_dir}/piper/piper --model {current_dir}/models/piper/en_GB-jenny_dioco-medium.onnx --output-raw | aplay -r 22050 -f S16_LE -t raw -"
while True:
tts_text = ans_text_q.get()
if bool_Chinese_tts:
command = f'echo "{tts_text}" | {piper_cmd_zh}'
else:
command = f"echo {shlex.quote(tts_text)} | {piper_cmd_en}"
process = subprocess.Popen(
command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
while True:
if stop_tts_event.is_set():
terminate_process(process.pid)
break
if process.poll() is not None:
break
time.sleep(0.01)
process.wait()
Piper TTS通過命令行進行調用,所以對于英文的文本要使用shlex.quote()函數,對特殊字符進行預處理,避免文本里的字符影響命令行的結構。同時,在播放過程中,如果錄音KEY被按下,則TTS會主動中斷退出。
顯示線程
def oled_thread(oled_device, dir):
with Image.open(f"{current_dir}/img/{dir}_logo.bmp") as img:
img_resized = img.convert("1").resize((128, 64))
img_resized = ImageOps.invert(img_resized)
oled_device.display(img_resized)
llama_load_done.wait()
senvc_load_done.wait()
frames_eye = []
durations_eye = []
with Image.open(f"{current_dir}/img/{dir}_eye.gif") as img:
for frame in ImageSequence.Iterator(img):
frames_eye.append(frame.convert("1").resize((128, 64)))
durations_eye.append(frame.info.get("duration", 100) / 1000.0)
frames_rcd = []
durations_rcd = []
with Image.open(f"{current_dir}/img/record.gif") as img:
for frame in ImageSequence.Iterator(img):
frames_rcd.append(frame.convert("1").resize((128, 64)))
durations_rcd.append(frame.info.get("duration", 100) / 1000.0)
while True:
if show_record_event.is_set():
for frame, duration in zip(frames_rcd, durations_rcd):
oled_device.display(frame)
time.sleep(duration)
if not show_record_event.is_set():
break
else:
for frame, duration in zip(frames_eye, durations_eye):
if model_doing_event.is_set() and duration > 1:
continue
if duration > 1:
duration = duration * 2
oled_device.display(frame)
show_record_event.wait(timeout=duration)
if show_record_event.is_set():
break
else:
if dir == "left":
oled_events["left"].set()
oled_events["right"].wait()
oled_events["right"].clear()
else:
oled_events["right"].set()
oled_events["left"].wait()
oled_events["left"].clear()
顯示線程主要用于協同顯示當前的程序運行狀態,例如錄音時顯示音頻波形、模型推理時顯示閉眼思考狀態,推理完成后睜開眼睛并眨眼等。
效果演示
圖片展示:
視頻演示:
B站鏈接:
代碼開源地址:
Gitee源代碼:
附加模型文件:
小結
本文介紹了在嵌入式終端上,基于本地大模型實現的多語言離線語音聊天機器人。本項目在樹莓派5上具體實現,代碼完全開源,理論上可以運行于任何具有相當算力和資源的嵌入式終端。