Trong khi OpenAI đang trì hoãn việc phát hành Chế độ giọng nói nâng cao cho ChatGPT, tôi muốn chia sẻ cách chúng tôi xây dựng ứng dụng giọng nói LLM và tích hợp nó vào một gian hàng tương tác.
Cuối tháng 2, Bali tổ chức lễ hội Lampu được sắp xếp theo nguyên tắc của Burning Man nổi tiếng. Theo truyền thống của nó, những người tham gia tạo ra các tác phẩm nghệ thuật và sắp đặt của riêng họ.
Những người bạn của tôi từ Trại 19:19 và tôi, được truyền cảm hứng từ ý tưởng về các tòa giải tội Công giáo và khả năng của LLM hiện tại, đã nảy ra ý tưởng xây dựng tòa giải tội AI của riêng chúng tôi, nơi bất kỳ ai cũng có thể nói chuyện với trí tuệ nhân tạo.
Đây là cách chúng tôi hình dung nó ngay từ đầu:
Để kiểm tra khái niệm và bắt đầu thử nghiệm lời nhắc cho LLM, tôi đã tạo một triển khai đơn giản trong một buổi tối:
Để triển khai bản demo này, tôi hoàn toàn dựa vào các mô hình đám mây từ OpenAI: Whisper , GPT-4 và TTS . Nhờ có thư viện tuyệt vời speech_recognition , tôi đã xây dựng bản demo chỉ trong vài chục dòng mã.
import os import asyncio from dotenv import load_dotenv from io import BytesIO from openai import AsyncOpenAI from soundfile import SoundFile import sounddevice as sd import speech_recognition as sr load_dotenv() aiclient = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY") ) SYSTEM_PROMPT = """ You are helpfull assistant. """ async def listen_mic(recognizer: sr.Recognizer, microphone: sr.Microphone): audio_data = recognizer.listen(microphone) wav_data = BytesIO(audio_data.get_wav_data()) wav_data.name = "SpeechRecognition_audio.wav" return wav_data async def say(text: str): res = await aiclient.audio.speech.create( model="tts-1", voice="alloy", response_format="opus", input=text ) buffer = BytesIO() for chunk in res.iter_bytes(chunk_size=4096): buffer.write(chunk) buffer.seek(0) with SoundFile(buffer, 'r') as sound_file: data = sound_file.read(dtype='int16') sd.play(data, sound_file.samplerate) sd.wait() async def respond(text: str, history): history.append({"role": "user", "content": text}) completion = await aiclient.chat.completions.create( model="gpt-4", temperature=0.5, messages=history, ) response = completion.choices[0].message.content await say(response) history.append({"role": "assistant", "content": response}) async def main() -> None: m = sr.Microphone() r = sr.Recognizer() messages = [{"role": "system", "content": SYSTEM_PROMPT}] with m as source: r.adjust_for_ambient_noise(source) while True: wav_data = await listen_mic(r, source) transcript = await aiclient.audio.transcriptions.create( model="whisper-1", temperature=0.5, file=wav_data, response_format="verbose_json", ) if transcript.text == '' or transcript.text is None: continue await respond(transcript.text, messages) if __name__ == '__main__': asyncio.run(main())
Những vấn đề chúng tôi phải giải quyết ngay lập tức trở nên rõ ràng sau những lần thử nghiệm đầu tiên của bản demo này:
Chúng tôi có quyền lựa chọn cách giải quyết những vấn đề này: bằng cách tìm kiếm giải pháp kỹ thuật hoặc sản phẩm phù hợp.
Trước khi viết mã, chúng tôi phải quyết định cách người dùng sẽ tương tác với gian hàng:
Để phát hiện người dùng mới trong gian hàng, chúng tôi đã xem xét một số tùy chọn: cảm biến mở cửa, cảm biến trọng lượng sàn, cảm biến khoảng cách và camera + mẫu YOLO. Đối với chúng tôi, cảm biến khoảng cách phía sau lưng có vẻ đáng tin cậy nhất vì nó loại trừ các tác nhân vô tình, chẳng hạn như khi cửa không đóng đủ chặt và không yêu cầu lắp đặt phức tạp, không giống như cảm biến trọng lượng.
Để tránh khó khăn khi nhận dạng phần đầu và phần cuối của hộp thoại, chúng tôi đã quyết định thêm một nút lớn màu đỏ để điều khiển micrô. Giải pháp này cũng cho phép người dùng làm gián đoạn AI bất cứ lúc nào.
Chúng tôi có nhiều ý tưởng khác nhau về việc triển khai phản hồi trong quá trình xử lý yêu cầu. Chúng tôi đã quyết định chọn một tùy chọn có màn hình hiển thị những gì hệ thống đang thực hiện: nghe micrô, xử lý câu hỏi hoặc trả lời.
Chúng tôi cũng cân nhắc một phương án khá thông minh với chiếc điện thoại cố định cũ. Phiên sẽ bắt đầu khi người dùng nhấc điện thoại và hệ thống sẽ lắng nghe người dùng cho đến khi anh ta gác máy. Tuy nhiên, chúng tôi quyết định rằng nó sẽ chân thực hơn khi người dùng được "trả lời" bằng buồng thay vì bằng giọng nói từ điện thoại.
Cuối cùng, luồng người dùng cuối cùng xuất hiện như thế này:
Arduino theo dõi trạng thái của cảm biến khoảng cách và nút màu đỏ. Nó gửi tất cả các thay đổi đến chương trình phụ trợ của chúng tôi thông qua API HTTP, cho phép hệ thống xác định xem người dùng đã vào hay rời khỏi gian hàng và liệu có cần kích hoạt nghe micrô hay bắt đầu tạo phản hồi hay không.
Giao diện người dùng web chỉ là một trang web được mở trong trình duyệt liên tục nhận trạng thái hiện tại của hệ thống từ phần phụ trợ và hiển thị nó cho người dùng.
Phần phụ trợ điều khiển micrô, tương tác với tất cả các mô hình AI cần thiết và đưa ra phản hồi LLM. Nó chứa logic cốt lõi của ứng dụng.
Cách viết mã bản phác thảo cho Arduino, kết nối chính xác cảm biến khoảng cách và nút và lắp ráp tất cả trong gian hàng là một chủ đề cho một bài viết riêng. Hãy xem xét ngắn gọn những gì chúng tôi nhận được mà không đi sâu vào chi tiết kỹ thuật.
Chúng tôi đã sử dụng Arduino, chính xác hơn là model ESP32 với mô-đun Wi-Fi tích hợp. Bộ vi điều khiển được kết nối với cùng mạng Wi-Fi với máy tính xách tay đang chạy chương trình phụ trợ.
Danh sách đầy đủ phần cứng chúng tôi đã sử dụng:
Các thành phần chính của quy trình là Chuyển giọng nói thành văn bản (STT), LLM và Chuyển văn bản thành giọng nói (TTS). Đối với mỗi nhiệm vụ, nhiều mô hình khác nhau có sẵn cả cục bộ và qua đám mây.
Vì chúng tôi không có sẵn GPU mạnh mẽ nên chúng tôi quyết định chọn phiên bản mô hình dựa trên đám mây. Điểm yếu của phương pháp này là cần có kết nối internet tốt. Tuy nhiên, tốc độ tương tác sau khi tối ưu hóa vẫn ở mức chấp nhận được, ngay cả với Internet di động mà chúng tôi có tại lễ hội.
Bây giờ, chúng ta hãy xem xét kỹ hơn từng thành phần của đường ống.
Nhiều thiết bị hiện đại từ lâu đã hỗ trợ nhận dạng giọng nói. Ví dụ: API Apple Speech có sẵn cho iOS và macOS và API Web Speech dành cho trình duyệt.
Thật không may, chúng có chất lượng rất kém so với Whisper hoặc Deepgram và không thể tự động phát hiện ngôn ngữ.
Để giảm thời gian xử lý, tùy chọn tốt nhất là nhận dạng giọng nói theo thời gian thực khi người dùng nói. Dưới đây là một số dự án có ví dụ về cách triển khai chúng:
Với máy tính xách tay của chúng tôi, tốc độ nhận dạng giọng nói bằng phương pháp này hóa ra khác xa so với thời gian thực. Sau một số thử nghiệm, chúng tôi đã quyết định sử dụng mô hình Whisper dựa trên đám mây của OpenAI.
Kết quả của mô hình Speech To Text ở bước trước là văn bản chúng tôi gửi tới LLM cùng với lịch sử hộp thoại.
Khi chọn LLM, chúng tôi đã so sánh GPT-3.5. GPT-4 và Claude. Hóa ra yếu tố quan trọng không phải là kiểu máy cụ thể mà là cấu hình của nó. Cuối cùng, chúng tôi đã quyết định chọn GPT-4, câu trả lời mà chúng tôi thích hơn những câu trả lời khác.
Việc tùy chỉnh lời nhắc cho các mô hình LLM đã trở thành một hình thức nghệ thuật riêng biệt. Có nhiều hướng dẫn trên Internet về cách điều chỉnh mô hình của bạn khi bạn cần:
Chúng tôi đã phải thử nghiệm nhiều cách cài đặt lời nhắc và nhiệt độ để làm cho mô hình phản hồi một cách hấp dẫn, chính xác và hài hước.
Chúng tôi nói lên phản hồi nhận được từ LLM bằng mô hình Chuyển văn bản thành giọng nói và phát lại cho người dùng. Bước này là nguyên nhân chính gây ra sự chậm trễ trong bản demo của chúng tôi.
LLM mất khá nhiều thời gian để phản hồi. Tuy nhiên, chúng hỗ trợ tạo phản hồi ở chế độ phát trực tuyến - từng mã thông báo. Chúng tôi có thể sử dụng tính năng này để tối ưu hóa thời gian chờ đợi bằng cách nói ra từng cụm từ khi chúng được nhận mà không cần đợi phản hồi đầy đủ từ LLM.
Chúng tôi sử dụng thời gian trong khi người dùng nghe đoạn đầu tiên để ẩn độ trễ trong việc xử lý các phần còn lại của phản hồi từ LLM. Nhờ cách tiếp cận này, độ trễ phản hồi chỉ xảy ra ở đầu và là ~ 3 giây.
async generateResponse(history) { const completion = await this.ai.completion(history); const chunks = new DialogChunks(); for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content; if (delta) { chunks.push(delta); if (chunks.hasCompleteSentence()) { const sentence = chunks.popSentence(); this.voice.ttsAndPlay(sentence); } } } const sentence = chunks.popSentence(); if (sentence) { this.voice.say(sentence); } return chunks.text; }
Ngay cả với tất cả các tối ưu hóa của chúng tôi, độ trễ 3-4 giây vẫn rất đáng kể. Chúng tôi quyết định chăm sóc giao diện người dùng bằng phản hồi để giúp người dùng khỏi cảm giác phản hồi bị treo. Chúng tôi đã xem xét một số cách tiếp cận:
Chúng tôi đã quyết định tùy chọn cuối cùng bằng một trang web đơn giản thăm dò phần phụ trợ và hiển thị hoạt ảnh theo trạng thái hiện tại.
Phòng xưng tội AI của chúng tôi đã hoạt động trong bốn ngày và thu hút hàng trăm người tham dự. Chúng tôi chỉ chi khoảng 50 USD cho API OpenAI. Đổi lại, chúng tôi đã nhận được những phản hồi tích cực đáng kể và những ấn tượng có giá trị.
Thử nghiệm nhỏ này cho thấy rằng có thể thêm giao diện giọng nói trực quan và hiệu quả vào LLM ngay cả với nguồn lực hạn chế và các điều kiện bên ngoài đầy thách thức.
Nhân tiện, các nguồn phụ trợ có sẵn trên GitHub