Web 端語音對話 AI 示例:使用 Whisper 和 llama.cpp 構建語音聊天機器人

凌虚發表於2024-11-24

大語言模型(LLM)為基於文字的對話提供了強大的能力。那麼,能否進一步擴充套件,將其轉化為語音對話的形式呢?本文將展示如何使用 Whisper 語音識別和 llama.cpp 構建一個 Web 端語音聊天機器人。

系統概覽

如上圖所示,系統的工作流程如下:

  1. 使用者透過語音輸入。
  2. 語音識別,轉換為文字。
  3. 文字透過大語言模型(LLM)生成文字響應。
  4. 最後,文字轉語音播放結果。

系統實現

端側的具體形態(如 web 端、桌面端、手機端)直接影響了第一步使用者語言的輸入,以及最後一步響應結果的語音播放。
在本文中,我們選擇使用 Web 端作為示例,利用瀏覽器本身的語言採集和語音播放功能,來實現使用者與系統的互動。

下圖展示了系統架構:

使用者透過 Web 端與系統互動,語音資料透過 WebSocket 傳輸到後端服務,後端服務使用 Whisper 將語音轉換為文字,接著透過 llama.cpp 呼叫 LLM 生成文字響應,最後,文字響應透過 WebSocket 傳送回前端,並利用瀏覽器的語音播放功能將其朗讀出來。

Web 端

Web 端的實現主要依賴 HTML5 和 JavaScript。我們使用瀏覽器的 Web API 進行語音採集和語音播放。以下是簡化的 Web 端程式碼示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Voice Chat AI</title>
    <style>
        #loading { display: none; font-weight: bold; color: blue }
        #response { white-space: pre-wrap; }
    </style>
</head>
<body>
    <h1>Voice Chat AI</h1>
    <button id="start">Start Recording</button>
    <button id="stop" disabled>Stop Recording</button>
    <p id="loading">Loading...</p>
    <p>AI Response: <span id="response"></span></p>

    <script>
        let audioContext, mediaRecorder;
        const startButton = document.getElementById("start");
        const stopButton = document.getElementById("stop");
        const responseElement = document.getElementById("response");
        const loadingElement = document.getElementById("loading");

        let socket = new WebSocket("ws://localhost:8765/ws");

        socket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            const inputText = data.input || "No input detected";
            responseElement.textContent += `\nUser said: ${inputText}`;
            const aiResponse = data.response || "No response from AI";
            responseElement.textContent += `\nAI says: ${aiResponse}\n`;
            loadingElement.style.display = "none";

            const utterance = new SpeechSynthesisUtterance(aiResponse);
            speechSynthesis.speak(utterance);
        };

        socket.onerror = (error) => {
            console.error("WebSocket error:", error);
            loadingElement.style.display = "none";
        };

        startButton.addEventListener("click", async () => {
            audioContext = new (window.AudioContext || window.webkitAudioContext)();
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            mediaRecorder = new MediaRecorder(stream);

            const audioChunks = [];
            mediaRecorder.ondataavailable = (event) => {
                audioChunks.push(event.data);
            };

            mediaRecorder.onstop = () => {
                const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
                loadingElement.style.display = "block";
                socket.send(audioBlob);
            };

            mediaRecorder.start();
            startButton.disabled = true;
            stopButton.disabled = false;
        });

        stopButton.addEventListener("click", () => {
            mediaRecorder.stop();
            startButton.disabled = false;
            stopButton.disabled = true;
        });
    </script>
</body>
</html>

為了簡化示例程式碼,使用了開始和結束按鈕來手動控制語音的錄製。如果要實現實時對話,除了需要合理設定語音採集的時間間隔,還需要確保後端能夠快速響應,避免延遲影響使用者體驗(這在我的膝上型電腦上無法做到)。

WebSocket 服務端

服務端實現為:

  • 使用 Pythonfastapi 框架搭建 WebSocket 服務。
  • 使用 whisper 進行語音識別,將語音轉換為文字,注意系統環境需要額外安裝 ffmpeg 命令列工具。
  • 透過 llama.cpp 載入 LLM(我使用的是 llama3.2-1B 模型) 並生成響應文字。

以下是服務端的程式碼示例:

from fastapi import FastAPI, WebSocket
import uvicorn
import whisper
import tempfile
import os
import signal

app = FastAPI()

# 載入 Whisper 模型,預設儲存位置 ~/.cache/whisper,可以透過 download_root 設定
model = whisper.load_model("base", download_root="WHISPER_MODEL")

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    try:
        await websocket.accept()
        while True:
            # 接收音訊資料
            audio_data = await websocket.receive_bytes()

            # 儲存臨時音訊檔案
            with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as temp_audio:
                temp_audio.write(audio_data)
                temp_audio_path = temp_audio.name

            # Whisper 語音識別
            result = model.transcribe(temp_audio_path)
            os.remove(temp_audio_path)
            text = result["text"]
            print("user input: ", text)

            # 生成 AI 回覆
            response_text = LLMResponse(text)
            print("AI response: ", response_text)

            await websocket.send_json({"input": text, "response": response_text})
    except Exception as e:
        print("Error: ", e)


def handle_shutdown(signal_num, frame):
    print(f"Received shutdown signal: {signal_num}")

def setup_signal_handlers():
    signal.signal(signal.SIGTERM, handle_shutdown)
    signal.signal(signal.SIGINT, handle_shutdown)

if __name__ == "__main__":
    setup_signal_handlers()

    config = uvicorn.Config("main:app", port=8765, log_level="info")
    server = uvicorn.Server(config)
    server.run()

此外,llama.cpp 使用 Docker 容器執行,作為 HTTP 服務來提供 LLM 的能力。啟動命令如下:

docker run -p 8080:8080 -v ~/ai-models:/models \
    ghcr.io/ggerganov/llama.cpp:server \
    -m /models/llama3.2-1B.gguf -c 512 \
    --host 0.0.0.0 --port 8080

WebSocket serverllama.cpp 之間則可以直接使用 HTTP 的方式通訊,示例程式碼如下:

import requests
import json

class LlamaCppClient:
    def __init__(self, host="http://localhost", port=8080):
        self.base_url = f"{host}:{port}"

    def completion(self, prompt):
        url = f"{self.base_url}/v1/chat/completions"
        headers = {"Content-Type": "application/json"}
        payload = {
            "messages": [
                {
                    "role": "system",
                    "content": """
                        You are a friendly conversation partner. Be natural, engaging, and helpful in our discussions. Respond to questions clearly and follow the conversation flow naturally.
                    """
                },
                {
                    "role": "user",
                    "content": prompt
                }
            ]
        }
        
        try:
            response = requests.post(url, headers=headers, data=json.dumps(payload))
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            return {"error": str(e)}

最後,使用者與 AI 的聊天結果類似下圖:

總結

透過結合 Web 端的語音識別和語音合成功能、Whisper 的語音轉文字能力、以及 llama.cpp 提供的 LLM 服務,我們成功構建了一個語音對話系統。語音對話的場景非常豐富,例如口語外教、語音問答等等。希望本文的示例能夠為你在構建語音互動式 AI 系統時提供啟發。


(我是凌虛,關注我,無廣告,專注技術,不煽動情緒,歡迎與我交流)


參考資料:

  • https://github.com/openai/whisper
  • https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md
  • https://github.com/fastapi/fastapi
  • https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthe...

相關文章