paint-brush
OpenAI का उपयोग करके एक सरल पॉप-अप चैटबॉट कैसे बनाएंद्वारा@alexxanderx
1,234 रीडिंग
1,234 रीडिंग

OpenAI का उपयोग करके एक सरल पॉप-अप चैटबॉट कैसे बनाएं

द्वारा Alexandru Prisacariu31m2024/06/30
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

इस ट्यूटोरियल में, मैं एक सरल और अधिक जटिल पॉपअप AI चैट बनाने का तरीका प्रस्तुत करूँगा जिसे किसी भी वेबसाइट पर जोड़ा जा सकता है। क्लाइंट बॉट को टाइप करके और बोलकर चैट का जवाब देने में सक्षम होगा। एप्लिकेशन को जावास्क्रिप्ट में लागू किया जाएगा और जटिल संस्करण के लिए हम वेबसॉकेट का उपयोग करेंगे।
featured image - OpenAI का उपयोग करके एक सरल पॉप-अप चैटबॉट कैसे बनाएं
Alexandru Prisacariu HackerNoon profile picture
0-item

इस ट्यूटोरियल में, मैं एक सरल पॉपअप AI चैट बनाने का तरीका प्रस्तुत करूँगा जिसे किसी भी वेबसाइट पर जोड़ा जा सकता है। क्लाइंट बॉट को टाइप करके और बोलकर चैट का जवाब देने में सक्षम होगा।


इस पोस्ट में बनाए गए चैटबॉट का प्रदर्शन

हम AI कार्यक्षमताओं के लिए OpenAI के उपकरणों का उपयोग करेंगे। चैट के लिए हम ChatGPT, STT (स्पीच-टू-टेक्स्ट) के लिए व्हिस्पर और TTS (टेक्स्ट-टू-स्पीच) के लिए उनके TTS का उपयोग करेंगे।


मैं ऐप को क्रियान्वित करने के लिए अनेक विधियां दिखाऊंगा, जो एक साधारण या बुनियादी विधि से लेकर एक बेहतर लेकिन अधिक जटिल विधि तक होगी।

यह एप्लीकेशन जावास्क्रिप्ट (ECMAScript) में लागू किया जाएगा। हालाँकि, यदि आप अन्य भाषाओं में कार्यान्वयन में रुचि रखते हैं, तो अंतिम अध्याय पढ़ें।


सभी कोड मेरे GitHub रिपॉजिटरी में उपलब्ध है।

मूल बातें

इस अध्याय में, हम एप्लिकेशन की मूल बातें जानेंगे: परियोजना संरचना और हमारे द्वारा उपयोग किये गए पैकेज।


परियोजना में निम्नलिखित पैकेजों का उपयोग किया जाएगा:

पैकेज का नाम

विवरण

express

HTTP सर्वर और रूटिंग के लिए

openai

OpenAI से जुड़ी सभी चीज़ों के लिए

sass

SASS स्टाइल फ़ाइलों को CSS फ़ाइलों में परिवर्तित करने के लिए

ws

वेबसॉकेट के लिए


परियोजना की संरचना इस प्रकार है:

पथ

विवरण

public

static नाम के तहत इंटरनेट पर प्रदर्शित निर्देशिका

public/audio

सार्वजनिक ऑडियो फ़ाइलें रखने वाली निर्देशिका

public/img

सार्वजनिक छवियों वाली निर्देशिका

public/index.html

प्रवेश बिंदु

style

पृष्ठ की शैली वाली निर्देशिका

version-1

सरल कार्यान्वयन स्रोत कोड निर्देशिका

version-2

बेहतर कार्यान्वयन स्रोत कोड निर्देशिका


इस परियोजना को यहां देखा जा सकता है, और जहां कोड सूचीबद्ध होगा, वहां उस कोड को ढूंढने का सापेक्ष पथ भी शामिल होगा।


SASS फ़ाइल को CSS में बदलने के लिए npm install चलाएँ और उसके बाद npm run build चलाएँ, और आप तैयार हैं।


सरल कार्यान्वयन शुरू करने के लिए, npm run start-v1 चलाएँ, या बेहतर कार्यान्वयन चलाने के लिए, npm run start-v2 चलाएँ। पर्यावरण चर OPENAI_API_KEY परिभाषित करना न भूलें।


UNIX सिस्टम पर आप चला सकते हैं:

 OPENAI_API_KEY=YOU_API_KEY npm run start-v1`

और विंडोज़ पर:

 set OPENAI_API_KEY=YOU_API_KEY npm run start-v1


जब आप इस पेज पर पहुंचेंगे तो आपको यह पेज दिखाई देगा:

वेबसाइट में प्रवेश करते समय आपको जो पृष्ठ दिखाई देता है


सरल/सरल कार्यान्वयन

सरल कार्यान्वयन सर्वर से डेटा भेजने और प्राप्त करने के लिए HTTP अनुरोधों और प्रतिक्रियाओं का उपयोग करता है।


हम प्रत्येक महत्वपूर्ण फ़ंक्शन पर नज़र डालेंगे। सभी कोड ऊपर दिए गए लिंक में पाए जा सकते हैं।


ऐप कैसे काम करेगा इसका एक गतिविधि आरेख यहां दिया गया है:

चैटबॉट कैसे काम करता है इसका UML आरेख


आइए देखें कि जब उपयोगकर्ता टेक्स्ट इनपुट तत्व पर एंटर दबाता है तो क्या होता है:


 /* version-1/client.js */ inputTextElement.addEventListener('keydown', async (event) => { if (event.code !== 'Enter') return; if (!inputTextElement.value) return; const message = inputTextElement.value; inputTextElement.value = ""; inputTextElement.disabled = true; inputSpeechElement.disabled = true; await addMessage('user', message); inputTextElement.disabled = false; inputSpeechElement.disabled = false; });


जब उपयोगकर्ता एंटर कुंजी दबाता है और इनपुट खाली नहीं होता है, तो हम टेक्स्ट इनपुट और ऑडियो बटन दोनों को अक्षम कर देंगे ताकि उपयोगकर्ता पिछले संदेश का जवाब मिलने तक दूसरा संदेश न भेज सके। जब हमें जवाब मिल जाता है, तो हम कार्यक्षमता बहाल कर देते हैं।


इनपुट को अक्षम करने के बाद, हम मुख्य फ़ंक्शन, addMessage कॉल करते हैं, जो जादू करता है। आइए इसे देखें:


 /* version-1/client.js */ /** * Add a new message to the chat. * @async * * @param {MessageType} type the type of message * @param {String|Audio} message the data of the message * @param {Object} [settings] additional settings * @param {Number} [settings.audioLength] the length of the audio in seconds * @returns {Promise} the promise resolved when all is done */ async function addMessage(type, message, settings = {}) { const newMsg = document.createElement('div'); newMsg.classList.add('message'); if (type === MessageType.User) { newMsg.classList.add('user'); newMsg.innerHTML = message; } else if (type === MessageType.UserAudio) { newMsg.classList.add('user', 'audio'); newMsg.innerHTML = 'Audio message'; } else { newMsg.classList.add(MessageType.Bot); } const msgsCnt = document.getElementById('friendly-bot-container-msgs'); msgsCnt.appendChild(newMsg); // Keeping own history log if (type === MessageType.User || type === MessageType.Bot) { messageHistory.push({ role: type === MessageType.User ? 'user' : 'assistant', content: message }); } if (type === MessageType.Bot) { if (Settings.UseWriteEffect) { // Create a write effect when the bot responds let speed = Settings.DefaultTypingSpeed; if (settings.audioLength) { const ms = settings.audioLength * 1000 + ((message.match(/,/g) || []).length * 40) + ((message.match(/\./g) || []).length * 70); speed = ms / message.length; } for (let i=0, length=message.length; i < length; i += 1) { newMsg.innerHTML += message.charAt(i); await sleep(speed); } } else { newMsg.innerHTML = message; } } else if (type === MessageType.User || type === MessageType.UserAudio) { let response; if (type === MessageType.User) { response = await sendMessage({ message }); } else if (type === MessageType.UserAudio) { response = await sendMessage({ audio: message }); } if (response.audio) { const audio = convertBase64ToAudio(response.audio); playAudio(audio); } return addMessage(MessageType.Bot, response.answer); } }


यह फ़ंक्शन नए संदेश के लिए एक नया HTMLDivElement बनाएगा और संदेश के प्रकार के आधार पर CSS वर्ग जोड़ेगा।


एक बार ऐसा हो जाने पर, हम संदेश को अपने क्लाइंट-साइड चैट इतिहास में संग्रहीत कर लेते हैं।


इसके बाद, यदि जोड़ा जाने वाला संदेश बॉट से है, तो हम इसे "लेखन प्रभाव" का उपयोग करके प्रदर्शित करते हैं। हम ऑडियो की लंबाई को संदेश में वर्णों की संख्या से विभाजित करके, यदि यह मौजूद है, तो टाइपिंग गति के साथ ऑडियो को सिंक्रनाइज़ करने का प्रयास करते हैं।


यदि जोड़ा गया संदेश उपयोगकर्ता से है, तो हम इसे sendMessage फ़ंक्शन को कॉल करके AI से उत्तर प्राप्त करने के लिए सर्वर पर भेजते हैं।


sendMessage फ़ंक्शन हमारे सर्वर पर fetch उपयोग करके एक HTTP अनुरोध करता है।


एक बात का उल्लेख करना आवश्यक है: हम प्रत्येक संदेश के साथ भेजे जाने वाले प्रत्येक ग्राहक के लिए एक यादृच्छिक आईडी तैयार करते हैं, ताकि सर्वर को पता रहे कि चैट इतिहास कहां से प्राप्त करना है।


सर्वर को पहचान-पहचान वाली आईडी भेजने का विकल्प यह है कि हर बार संपूर्ण इतिहास भेजा जाए, लेकिन प्रत्येक संदेश के साथ, भेजे जाने वाले डेटा की संख्या बढ़ती जाती है।


 /* version-1/client.js */ /** * Create a random ID of given length. * Taken from https://stackoverflow.com/a/1349426 * * @param {Number} length the length of the generated ID * @returns {String} the generated ID */ function makeID(length) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const charactersLength = characters.length; let counter = 0; while (counter < length) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); counter += 1; } return result; } const ChatID = makeID(10); // ... /** * Send a message to the server and return the JSON back. * @async * * @param {Object} data the data to send * @returns {Promise<Object>} the result from the server */ async function sendMessage(data = {}) { try { const response = await fetch(Settings.APISendMessage, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id: ChatID, ...data }), }); return response.json(); } catch (error) { console.error("Error:", error); } }


इससे पहले कि हम सर्वर साइड पर जाकर देखें कि यह अनुरोध कैसे संभालता है, आइए देखें कि जब उपयोगकर्ता ऑडियो बटन पर क्लिक करता है तो क्या होता है:


 /* version-1/client.js */ inputSpeechElement.addEventListener('click', async (_event) => { inputTextElement.value = ""; inputTextElement.disabled = true; inputSpeechElement.disabled = true; const stopRecordButtonElement = document.getElementById('friendly-bot-container-stop-record'); const base64Audio = await recordUserAudio(stopRecordButtonElement); await addMessage(MessageType.UserAudio, base64Audio.substring(`data:${Settings.ClientAudioMimeType};base64,`.length)); inputTextElement.disabled = false; inputSpeechElement.disabled = false; });


यह टेक्स्ट इनपुट हैंडलिंग के बहुत समान है। फ़ंक्शन recordUserAudio एक बेस64 एनकोडेड ऑडियो लौटाएगा, और हम इसे addMessage पर भेजने से पहले इसके हेडर को काट देंगे।


recordUserAudio फ़ंक्शन ऑडियो रिकॉर्ड करने के लिए उपयोगकर्ता से अनुमति लेने की कोशिश करेगा और, अगर अनुमति मिल जाती है, तो एक MediaRecorder बनाएगा और रिकॉर्डिंग शुरू करेगा। हम उपयोगकर्ता को यह बताने के लिए कुछ UI तत्व भी दिखाएंगे कि हम उनकी आवाज़ रिकॉर्ड कर रहे हैं और रिकॉर्डिंग पूरी होने पर उसे रोकने के लिए एक बटन भी दिखाएंगे।


एक बार स्टॉप बटन दबाने पर, हम ऑडियो खंड को Blob ऑब्जेक्ट में और ब्लॉब को बेस64 एनकोडेड स्ट्रिंग में परिवर्तित कर देते हैं और उसे वापस कर देते हैं।


हम प्रत्येक ऑडियो ट्रैक को देखते हैं और उन्हें रोकते हैं, फिर उन्हें हटा देते हैं। यह इसलिए ज़रूरी है क्योंकि, कम से कम क्रोम पर, mediaRecorder.stop() को कॉल करने से माइक्रोफ़ोन की "सुनने" की स्थिति बंद नहीं होगी।


ऑडियो को base64 में एनकोड करना ऑडियो को सर्वर पर भेजने का बहुत कुशल तरीका नहीं है, लेकिन यह बहुत आसान तरीका है। हम बेहतर कार्यान्वयन अनुभाग में ऑडियो को सर्वर पर भेजने के लिए एक और तरीका देखेंगे।


 /* version-1/client.js */ /** * Record the user and return it as an base64 encoded audio. * @async * * @param {HTMLElement} stopRecordButtonElement the stop button element * @returns {Promise<String>} the base64 encoded audio */ async function recordUserAudio(stopRecordButtonElement) { let stream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (error) { console.error(`The following getUserMedia error occurred: ${error}`); return; } let chunks = []; const mediaRecorder = new MediaRecorder(stream, { mimeType: Settings.ClientAudioMimeType }); return new Promise((resolve, reject) => { const onStopClick = () => { mediaRecorder.stop(); stopRecordButtonElement.classList.remove('show'); }; mediaRecorder.addEventListener('dataavailable', (event) => { chunks.push(event.data); }); mediaRecorder.addEventListener('stop', (_event) => { const blob = new Blob(chunks, { type: Settings.ClientAudioMimeType }); chunks = []; const base64AudioPromise = blobToBase64(blob); stopRecordButtonElement.removeEventListener('click', onStopClick); // Stop the audio listening stream.getAudioTracks().forEach((track) => { track.stop() stream.removeTrack(track); }); base64AudioPromise.then(resolve).catch(reject); }); stopRecordButtonElement.classList.add('show'); mediaRecorder.start(); stopRecordButtonElement.addEventListener('click', onStopClick); }) }


आइए अब देखें कि सर्वर पर अनुरोध कैसे संभाला जाता है:


 /* version-1/server.js */ app.post('/api/message', async (req, res) => { if (!req.body.message && !req.body.audio) { res.status(400).send('Missing "message" or "audio"'); } if (req.body.message && req.body.audio) { res.status(400).send('Cannot be both "message" and "audio"'); } if (!req.body.id) { res.status(400).send('Missing "id"'); } const messages = getChatMessages(chatHistory, req.body.id); if (req.body.message) { messages.push({ role: "user", content: req.body.message }); } else { let content; try { content = await stt(openai, req.body.audio); } catch (error) { console.error(`(ChatID: ${req.body.id}) Error when trying to convert the user's audio to text:`); console.error(error); res.status(500).end(); return; } messages.push({ role: "user", content }); } let answer; try { answer = await getAnswer(openai, messages); } catch (error) { console.error(`(ChatID: ${req.body.id}) Error when trying to get an answer from ChatGPT:`); console.error(error); res.status(500).end(); return; } let audio; if (Settings.CreateAudioOfAnswer) { try { audio = await tts(openai, answer); } catch (error) { console.error(`(ChatID: ${req.body.id}) Error when trying to convert the ChatGPT's answer to audio:`); console.error(error); res.status(500).end(); return; } } messages.push({ role: "assistant", content: answer }); res.json({ answer, audio }); });


जब हम यह जांच लेते हैं कि क्लाइंट ने आवश्यक डेटा भेजा है, तो हमें दी गई आईडी के लिए चैट इतिहास मिलता है (या एक नया चैट इतिहास बनाते हैं):


 /* version-1/server.js */ /** * Get the chat history, or create a new one or the given ID. * * @param {Object} chatHistory the global chat history object containing all the chats * @param {String} id the ID of the chat to retrieve * @returns {Object} the chat history for the given `id` */ function getChatMessages(chatHistory, id) { if (!chatHistory[id]) { chatHistory[id] = [ { role: "system", content: Settings.AISystemContent }, { role: "assistant", content: Settings.WelcomeMessage } ]; } return chatHistory[id]; }


फिर, यदि प्राप्त संदेश ऑडियो नहीं बल्कि टेक्स्ट है, तो हम संदेश को चैट इतिहास में जोड़ देते हैं। यदि प्राप्त संदेश ऑडियो है, तो हम फ़ंक्शन stt कॉल करते हैं जो OpenAI के व्हिस्पर का उपयोग करके स्पीच-टू-टेक्स्ट क्रिया निष्पादित करेगा।


फ़ंक्शन openai.audio.transcriptions.create विधि का उपयोग करेगा। इस विधि का मुख्य पैरामीटर file है, जिसे हमारे ऑडियो डेटा का प्रतिनिधित्व करना चाहिए। हम अपने बेस64 एनकोडेड ऑडियो फ़ाइल को एक ऐसी फ़ाइल में बदलने के लिए openai/uploads पैकेज से toFile फ़ंक्शन का उपयोग करते हैं जिसे OpenAI पढ़ सकता है। फ़ंक्शन दिए गए ऑडियो का ट्रांसक्रिप्शन लौटाएगा।


 /* version-1/server.js */ /** * Convert speech to text using OpenAI. * @async * * @param {OpenAI} openai the OpenAI instance * @param {String} audio the base64 encoded audio * @returns {Promise<String>} the text */ async function stt(openai, audio) { // Documentation https://platform.openai.com/docs/api-reference/audio/createTranscription const transcription = await openai.audio.transcriptions.create({ file: await toFile(Buffer.from(audio, 'base64'), `audio.${Settings.ClientAudioExtension}`), model: Settings.STTModel, language: Settings.ClientAudioLanguage, // this is optional but helps the model }); return transcription.text; }


अब जब हमारे पास संदेश आ गया है, तो हम चैट को ChatGPT पर भेजते हैं और getAnswer फ़ंक्शन को कॉल करके प्रतिक्रिया की प्रतीक्षा करते हैं।


 /* version-1/server.js */ /** * * @param {*} openai * @param {*} messages * @returns */ async function getAnswer(openai, messages) { // Documentation https://platform.openai.com/docs/api-reference/chat/create const completion = await openai.chat.completions.create({ messages, model: Settings.ChatGPTModel, }); return completion.choices[0].message.content; }


अंतिम भाग tts फ़ंक्शन का उपयोग करके AI से प्रतिक्रिया को ऑडियो में परिवर्तित करने के बारे में है जो ऑडियो फ़ाइल बनाने के लिए openai.audio.speech.create विधि का उपयोग करता है। OpenAI का TTS कई प्रारूपों का समर्थन करता है, लेकिन हमने इस ट्यूटोरियल के लिए mp3 चुना है।


एक बार ऑडियो डेटा प्राप्त हो जाने पर, हम इसे Buffer में परिवर्तित करते हैं, और वहां से, क्लाइंट को वापस भेजने के लिए बेस 64 एनकोडेड ऑडियो स्ट्रिंग में परिवर्तित करते हैं।


 /* version-1/server.js */ /** * Convert text to speech. * @async * * @param {*} openai * @param {*} input * @returns */ async function tts(openai, input) { // Documentation https://platform.openai.com/docs/api-reference/audio/createSpeech const mp3 = await openai.audio.speech.create({ model: Settings.TTSModel, voice: Settings.TTSVoice, input, response_format: Settings.TTSFormat }); return Buffer.from(await mp3.arrayBuffer()).toString('base64'); }


बेहतर कार्यान्वयन

लेकिन क्या हम इसे बेहतर बना सकते हैं? हाँ, हाँ। HTTP अनुरोधों का उपयोग करने के बजाय, हम क्लाइंट और सर्वर के बीच संचार करने के लिए WebSockets का उपयोग कर सकते हैं और ChatGPT को परिणामों को स्ट्रीम के रूप में वापस करने के लिए कह सकते हैं। इस तरह, हम एक वास्तविक समय लेखन प्रभाव बना सकते हैं क्योंकि हम ChatGPT से क्लाइंट को वास्तविक समय में परिणाम स्ट्रीम करेंगे।


इस कार्यान्वयन में एक खामी है, लेकिन केवल इसलिए क्योंकि हम OpenAI के TTS का उपयोग कर रहे हैं, जो प्रति मिनट अधिकतम 3 अनुरोध स्वीकार करता है। इसलिए, इस कार्यान्वयन के लिए, हम टेक्स्ट-टू-स्पीच सुविधा को छोड़ देंगे, लेकिन मैं इसे फिर से लागू करने के तरीके और ऐसा करते समय किन बातों पर ध्यान देना है, इस पर सुझाव दूंगा।


तो चलिए कुछ कोड देखते हैं। हमने पिछले कार्यान्वयन से शुरुआत की और वेबसॉकेट को सपोर्ट करने के लिए जो ज़रूरी था, उसे बदल दिया।


 /* version-2/client.js */ const ws = new WebSocket(Settings.WSAddress); const ChatID = makeID(10); // When the connection to the server is made send the chat ID ws.addEventListener('open', () => { const idMessage = new Uint8Array([ClientMessageID.SetClientID, ...new TextEncoder().encode(ChatID)]); ws.send(idMessage); });


क्लाइंट कोड के इस भाग में, हम WebSocket सर्वर से जुड़ते हैं। जब कनेक्शन खोला जाता है, तो हम पहले संदेश के रूप में चैट आईडी भेजते हैं ताकि सर्वर को पता चले कि हम कौन हैं।


क्लाइंट और सर्वर के बीच भेजे गए डेटा/संदेशों की संरचना इस प्रारूप का अनुसरण करती है:


संदेशों की संरचना


पहला बाइट उस संदेश के प्रकार को दर्शाता है जिसे हम भेज रहे हैं, जिससे सर्वर को यह पता चलता है कि भेजे गए डेटा में निम्नलिखित बाइट्स द्वारा दर्शाए गए पेलोड को कैसे संभालना है।


ध्यान दें कि हमने WebSocket सर्वर को केवल बाइनरी डेटा स्वीकार करने और भेजने के लिए कॉन्फ़िगर किया है। यही कारण है कि हम हमेशा क्लाइंट साइड से Uint8Array और सर्वर साइड से Buffer भेजेंगे। हम केवल बाइनरी में iy भेज रहे हैं क्योंकि यह अधिक कुशल है, केवल वही टेक्स्ट में परिवर्तित होता है जिसकी हमें आवश्यकता होती है और बाइनरी में ही रहता है (जैसे ऑडियो चंक्स, जिसे बाइनरी में रहने की आवश्यकता होती है)।


निम्नलिखित कोड में, हम सर्वर साइड से प्राप्त संदेशों को संभालते हैं:


 /* version-2/client.js */ const subscriptionsToWSMessages = []; ws.addEventListener('message', async (event) => { const data = new Uint8Array(await event.data.arrayBuffer()); const messageType = data[0]; // Because we know all the possible messages are all strings we can convert all the payloads to string const content = new TextDecoder().decode(data.slice(1)); if (!ws.allGood && messageType !== ServerMessageID.OK) { if (messageType === ServerMessageID.Error) { console.error('Something wrong sending the chat ID:', content); } } else if (messageType === ServerMessageID.OK) { ws.allGood = true; } else { let done; for (let i=0, length=subscriptionsToWSMessages.length; i < length; i += 1) { done = await subscriptionsToWSMessages[i](messageType, content); if (done === true) return; } if (!done) { if (messageType === ServerMessageID.Error) { console.error('Unhandled error received from server:', content); } else { console.log(`Unknown message type "${messageType}" received.`); } } } });


चूंकि हम जानते हैं कि सर्वर साइड से प्राप्त सभी संदेश पाठ हैं, हम TextDecoder का उपयोग करके पूरे पेलोड को सुरक्षित रूप से String में परिवर्तित कर सकते हैं: new TextDecoder().decode(data.slice(1));


सबसे पहले, हम सर्वर से पहले ServerMessageID.OK की प्रतीक्षा करेंगे, जो दर्शाता है कि भेजी गई चैट आईडी वैध है।


लचीलापन बनाए रखने के लिए, हम फ़ंक्शन की एक सरणी का उपयोग करते हैं जो सर्वर से प्राप्त संदेशों के श्रोताओं का प्रतिनिधित्व करते हैं। यह हमें अपने दृष्टिकोण में मॉड्यूलर होने की अनुमति देता है। प्रत्येक फ़ंक्शन को true या false लौटना चाहिए: true अर्थ है कि संदेश संसाधित किया गया था, और बाकी सब्सक्राइब किए गए फ़ंक्शन को कॉल करना आवश्यक नहीं है।


ग्राहकों को जोड़ना और हटाना आसान बनाने के लिए, हम अपने ws ऑब्जेक्ट को निम्नलिखित के साथ विस्तारित करते हैं:


 /* version-2/client.js */ /** * Add a function to the list of functions to be called when the socket receives * a new message. The function must return a boolean: if `true` is returned then * is considered that the message was handled and will stop the exection of the * rest of the subscribers in the list. * * @param {Function} fn the function to be added */ ws.subscribeToWSMessage = (fn) => { subscriptionsToWSMessages.push(fn); } /** * Remove an added function from the list of subscribers. * * @param {Function} fn the function to be removed */ ws.unsubscribeToWSMessage = (fn) => { subscriptionsToWSMessages.splice(subscriptionsToWSMessages.indexOf(fn), 1); }


इसके बाद, हम ws ऑब्जेक्ट को 3 और विधियों के साथ विस्तारित करते हैं:

  • उपयोगकर्ता का पाठ संदेश भेजने के लिए sendTextMessage ;


  • उपयोगकर्ता की ध्वनि रिकॉर्डिंग से ऑडियो खंड भेजने के लिए sendAudioChunk ;


  • sendAudioEnd सर्वर को यह बताने के लिए कि ऑडियो समाप्त हो गया है।


 /* version-2/client.js */ /** * Send a text message to the server. * @async * * @param {String} message the message to send * @param {Function} onNewMessageContent the function to be called with the new answer from bot as it sent from the server */ ws.sendTextMessage = async (message, onNewMessageContent) => { ws.createSubscriptionForBotResponse(onNewMessageContent); const wsMessage = new Uint8Array([ClientMessageID.UserTextMessage, ...new TextEncoder().encode(message)]); ws.send(wsMessage); };


sendTextMessage विधि उस संदेश को स्वीकार करती है जिसे सर्वर पर भेजने की आवश्यकता होती है और एक फ़ंक्शन जिसे ChatGPT से प्राप्त डेटा की स्ट्रीम के साथ कई बार कॉल किया जाएगा।


इस विधि में, सर्वर को संदेश भेजने से पहले, हम createSubscriptionForBotResponse विधि को कॉल करते हैं, जो बॉट से प्रतिक्रिया को संभालने के लिए नए संदेशों को सुनने के लिए सदस्यता बनाने और जोड़ने का काम करती है।


 /* version-2/client.js */ /** * Create and add a subscription to listen for the response of the bot to our sent message * * @param {Function} onNewMessageContent the function to be called with the new answer from bot as it sent from the server */ ws.createSubscriptionForBotResponse = (onNewMessageContent) => { const wsMessagesHandler = (messageType, content) => { if (messageType === ServerMessageID.TextChunk) { onNewMessageContent(content); return true; } else if (messageType === ServerMessageID.TextEnd) { ws.unsubscribeToWSMessage(wsMessagesHandler); return true; } return false; } ws.subscribeToWSMessage(wsMessagesHandler); }


सब्सक्राइब्ड फ़ंक्शन जाँच करेगा कि सर्वर से प्राप्त संदेश में बॉट की प्रतिक्रिया के लिए आवश्यक संदेश प्रकार है या नहीं ( ServerMessageID.TextChunk )। यदि ऐसा है, तो हम प्राप्त फ़ंक्शन को टेक्स्ट चंक के साथ कॉल करते हैं, जो चैट में वर्तमान बॉट प्रतिक्रिया में चंक को जोड़ देगा।


जब बॉट प्रतिक्रिया दे देता है, तो सर्वर हमें ServerMessageID.TextEnd प्रकार का एक संदेश भेजेगा, जो यह संकेत देगा कि हम सुनना बंद कर सकते हैं, जिस बिंदु पर, हम नए संदेशों को सुनने से सदस्यता समाप्त कर देंगे।


 /* version-2/client.js */ /** * Send an audio chunk to the server. * @async * * @param {Blob} blobChunk the audio blob chunk */ ws.sendAudioChunk = async (blobChunk) => { const wsMessage = new Uint8Array([ClientMessageID.UserAudioChunk, ...new Uint8Array(await blobChunk.arrayBuffer())]); ws.send(wsMessage); }; /** * Tell the server that the audio is done. * * @param {Function} onNewMessageContent the function to be called with the new answer from bot as it sent from the server */ ws.sendAudioEnd = (onNewMessageContent) => { ws.createSubscriptionForBotResponse(onNewMessageContent); ws.send(new Uint8Array([ClientMessageID.UserAudioEnd])); };


अगली 2 विधियाँ, sendAudioChunk और sendAudioEnd , उपयोगकर्ता की रिकॉर्ड की गई आवाज़ को सर्वर पर भेजने के लिए हैं। पहला, sendAudioChunk , प्राप्त बाइट्स को सर्वर पर भेजेगा, जबकि दूसरा, sendAudioEnd , सर्वर को एक संदेश भेजेगा जो यह संकेत देगा कि ऑडियो समाप्त हो गया है और, sendTextMessage विधि की तरह, बॉट से प्रतिक्रिया सुनने के लिए createSubscriptionForBotResponse कॉल करेगा।


आगे, हम देखेंगे कि sendTextMessage और sendAudioEnd विधियों से onNewMessageContent पैरामीटर कैसे भेजा जाता है।


हमने addMessage फ़ंक्शन को addUserMessage और addBotMessage में विभाजित करके थोड़ा संशोधित किया है। हम केवल addUserMessage को देखेंगे:


 /* version-2/client.js */ /** * Add a new message to the chat. * @async * * @param {WebSocket} ws the WebSocket * @param {MessageType} type the type of message * @param {String|Audio} message the data of the message * @returns {Promise} the promise resolved when all is done */ async function addUserMessage(ws, type, message) { createMessageHTMLElement(type, type === MessageType.User ? message : 'Audio message'); // Keeping own history log if (type === MessageType.User) { messageHistory.push({ role: type === MessageType.User ? 'user' : 'assistant', content: message }); } if (type === MessageType.User) { await ws.sendTextMessage(message, addBotMessageInChunks()); } else { await ws.sendAudioEnd(addBotMessageInChunks()); } } /** * Add bot message in chunks. The functions returns another function that when called with * the argument will add that argument to the bot message. * * @returns {Function} the function accept a parameter `content`; when called the `content` is added to the message */ function addBotMessageInChunks() { const newMsg = createMessageHTMLElement(MessageType.Bot); let nextContentIndex = 0; let currentContentIndex = 0; let currentContentPromise; const onNewMessageContent = async (content) => { const thisContentIndex = nextContentIndex; nextContentIndex += 1; while (thisContentIndex !== currentContentIndex) { await currentContentPromise; } currentContentPromise = new Promise(async resolve => { await addContentToMessage(newMsg, content); currentContentIndex += 1; resolve(); }); } return onNewMessageContent; }


addBotMessageInChunks फ़ंक्शन उस फ़ंक्शन को बनाने और वापस करने के लिए ज़िम्मेदार है जो दिए गए टेक्स्ट/सामग्री को वर्तमान बॉट संदेश में जोड़ देगा।


क्योंकि हम बॉट संदेश पर लेखन प्रभाव चाहते हैं क्योंकि यह आता है, इसलिए हमें सब कुछ सिंक्रनाइज़ करने के लिए एक विधि की आवश्यकता है। सर्वर पाठ को वैसे ही भेजेगा जैसे वह आता है, और addContentToMessage फ़ंक्शन, जो लेखन प्रभाव बनाने के लिए ज़िम्मेदार है, अगले प्राप्त पाठ को संभालने के लिए समय पर तैयार नहीं हो सकता है।


इसलिए, हम एक सरल सिंक्रोनाइज़ेशन तंत्र के साथ आए: हम 2 काउंटर और एक वैरिएबल बनाते हैं जो एक वादा रखेगा। हर बार जब रिटर्न किए गए फ़ंक्शन को कॉल किया जाता है, तो हम उस कॉल को अगला इंडेक्स (लाइन 39) असाइन करते हैं, और फिर काउंटर बढ़ाते हैं। फ़ंक्शन वादे के हल होने की प्रतीक्षा करके अपनी बारी का इंतज़ार करेगा, और जब उसकी बारी आएगी, तो वह वादे के वैरिएबल को एक नए वादे से अधिलेखित कर देगा जो केवल लेखन प्रभाव के पूरा होने की प्रतीक्षा करेगा (लाइन 47) और फिर काउंटर बढ़ाएगा।


 /* version-2/client.js */ /** * Record the user and send the chunks to the server and on end wait for all the chunks to be sent. * @async * * @param {WebSocket} ws the WebSocket * @param {HTMLElement} stopRecordButtonElement the stop button element * @returns {Promise} */ async function recordUserAudio(ws, stopRecordButtonElement) { let stream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (error) { console.error(`The following getUserMedia error occurred: ${error}`); return; } const mediaRecorder = new MediaRecorder(stream, { mimeType: Settings.ClientAudioMimeType }); return new Promise((resolve, _reject) => { const onStopClick = () => { mediaRecorder.stop(); stopRecordButtonElement.classList.remove('show'); }; // Create an array to store the promises of the sent audio chunks so we can make sure that // when the user hit the stop button all the audio chunks are sent const sentAudioChunksPromises = []; mediaRecorder.addEventListener('dataavailable', (event) => { sentAudioChunksPromises.push(ws.sendAudioChunk(event.data)); }); mediaRecorder.addEventListener('stop', async (_event) => { await Promise.all(sentAudioChunksPromises); // Stop the audio listening stream.getAudioTracks().forEach((track) => { track.stop() stream.removeTrack(track); }); resolve(); }); stopRecordButtonElement.classList.add('show'); // The parameter of `start` is called `timeslice` and define how often, in milliseconds, // to fire the `dataavailable` event with the audio chunk mediaRecorder.start(1000); stopRecordButtonElement.addEventListener('click', onStopClick); }) }


फ़ंक्शन recordUserAudio भी थोड़ा परिवर्तन किया गया:

  • तर्क 1000 के साथ mediaRecorder.start() को कॉल करने से उपयोगकर्ता के ऑडियो को 1 सेकंड के टुकड़ों में विभाजित किया जाएगा, जो dataavailable इवेंट के लिए हैंडलर में प्राप्त किया जाएगा;


  • dataavailable इवेंट के लिए हैंडलर ws.sendAudioChunk के कॉलिंग द्वारा लौटाए गए वादे को एक सरणी में जोड़ देगा, ताकि हम अपने MediaRecorder इंस्टेंस के stop इवेंट के लिए हैंडलर में उन सभी के समाप्त होने की प्रतीक्षा कर सकें।


क्लाइंट-साइड के लिए यह काफी है।


अब, आइए सर्वर साइड पर जाएं और देखें कि क्या जोड़ा गया है:


 /* version-2/server.js */ const webSocketServer = new WebSocketServer({ port: Settings.WSPort }); webSocketServer.on('connection', function connection(clientWS) { // Array to keep all the audio chunks until the user is done talking clientWS.audioChunks = []; clientWS.on('error', console.error); clientWS.on('message', async (data, isBinary) => { // ... }); });


हम अपने निर्धारित पोर्ट के साथ वेबसॉकेट सर्वर ( ws पैकेज का उपयोग करके) बना रहे हैं। एक बार जब हमारे पास कनेक्शन हो जाता है, तो हम क्लाइंट सॉकेट में audioChunks नामक एक खाली सरणी जोड़ते हैं, जो ऑडियो बफर चंक्स को होल्ड करेगा।


जब उपयोगकर्ता कोई संदेश भेजता है, तो हम निम्नलिखित कार्य करते हैं:


 /* version-2/server.js */ // ... // If the message is non-binary then reject it. // If the user did not already set the chatID then we close the socket. if (!isBinary) { const errorMsg = 'Only binary messages are supported.'; clientWS.send(Buffer.from([ServerMessageID.Error, errorMsg])); console.error(`(ChatID: ${clientWS.chatID}) Non-binary message received.`); if (!clientWS.chatID) { clientWS.close(1003, errorMsg); } return; } const messageType = data[0]; const payload = data.slice(1); if (!clientWS.chatID && messageType !== ClientMessageID.SetClientID) { clientWS.send(Buffer.from('Error! Please send first your ID')); } else if (messageType === ClientMessageID.SetClientID) { const id = payload.toString('utf8'); if (typeof id === 'string' && id.trim() !== '') { clientWS.chatID = id; clientWS.send(Buffer.from([ServerMessageID.OK])); } else { clientWS.send(Buffer.from([ServerMessageID.Error, ...Buffer.from('Error! Invalid ID. Please send a valid string ID.')])); } } // ...


सबसे पहले, हम जाँचते हैं कि प्राप्त संदेश बाइनरी में है या नहीं। उसके बाद, हम संदेश प्रकार ( messageType ) को बाकी डेटा ( payload ) से अलग करते हैं। अगर क्लाइंट ने अभी तक चैट आईडी नहीं भेजी है और संदेश प्रकार इसके लिए नहीं है, तो एक त्रुटि लौटाएँ। अन्यथा, अगर चैट आईडी सही है तो हम क्लाइंट सॉकेट के अंदर उसे स्टोर करते हैं।


 /* version-2/server.js */ // ... } else if (messageType === ClientMessageID.UserTextMessage || messageType === ClientMessageID.UserAudioEnd) { const messages = getChatMessages(chatHistory, clientWS.chatID); let messageContent; if (messageType === ClientMessageID.UserTextMessage) { messageContent = payload.toString('utf8'); } else if (messageType === ClientMessageID.UserAudioEnd) { // When the client send the `ClientMessageID.UserAudioEnd` message type it means it clicked the STOP button // Concat all the buffers into a single one const buffer = Buffer.concat(clientWS.audioChunks); // Reset the chunks array clientWS.audioChunks = []; // Send audio to OpenAI to perform the speech-to-text messageContent = await stt(openai, buffer); } messages.push({ role: "user", content: messageContent }); try { await getAnswer(openai, messages, clientWS); } catch (error) { console.error(`(ChatID: ${clientWS.chatID}) Error when trying to get an answer from ChatGPT:`); console.error(error); clientWS.send(Buffer.from([ServerMessageID.Error, ...Buffer.from('Error!')])); return; } } // ...


जब क्लाइंट ClientMessageID.UserTextMessage या ClientMessageID.UserAudioEnd प्रकार का संदेश भेजता है, तो हम पहले की तरह चैट के संदेशों को पुनः प्राप्त करते हैं। यदि संदेश ClientMessageID.UserTextMessage प्रकार का है, तो हम प्राप्त डेटा ( payload ) को String में बदल देंगे।


यदि संदेश ClientMessageID.UserAudioEnd प्रकार का है, तो हम सभी ऑडियो बफर खंडों को एक खंड में संयोजित करेंगे, खंडों की सरणी को रीसेट करेंगे, और ऑडियो पर वाक्-से-पाठ क्रिया निष्पादित करेंगे, जो पाठ लौटाएगा।


अगला चरण ChatGPT द्वारा स्वीकृत प्रारूप में नया संदेश बनाना और प्रतिक्रिया के लिए ChatGPT से पूछताछ करना है।


 /* version-2/server.js */ // ... } else if (messageType === ClientMessageID.UserAudioChunk) { clientWS.audioChunks.push(payload); } // ...


अंतिम संदेश प्रकार जिसे हम संभालते हैं वह ऑडियो चंक्स के लिए है, जो प्राप्त डेटा को क्लाइंट सॉकेट के ऑडियो चंक्स सरणी में जोड़ता है।


अब, आइए देखें कि स्ट्रीम्स का समर्थन करने के लिए getAnswer फ़ंक्शन को कैसे बदला गया:


 /* version-2/server.js */ /** * Get the next message in the conversation * @async * * @param {OpenAI} openai the OpenAI instance * @param {String[]} messages the messages in the OpenAI format * @returns {String} the response from ChatGPT */ async function getAnswer(openai, messages, clientWS) { // Documentation https://platform.openai.com/docs/api-reference/chat/create const stream = await openai.chat.completions.create({ model: Settings.ChatGPTModel, messages, stream: true, }); for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content; if (!content) continue; clientWS.send(Buffer.from([ServerMessageID.TextChunk, ...Buffer.from(content || "")])); } clientWS.send(Buffer.from([ServerMessageID.TextEnd])); }


ChatGPT को एक तर्क के रूप में भेजे गए ऑब्जेक्ट में बस stream: true जोड़कर, यह एक स्ट्रीम ऑब्जेक्ट लौटाएगा जिसे हम लूप कर सकते हैं। डेटा के प्रत्येक गैर-खाली हिस्से के लिए, हम इसे क्लाइंट को वापस भेज देंगे। स्ट्रीम पूरा होने के बाद, हमें क्लाइंट को सूचित करना होगा कि प्रतिक्रिया पूरी हो गई है।

TTS कार्यक्षमता को वापस जोड़ने के लिए सुझाव

ठीक है, ठीक है, लेकिन क्या होगा यदि हमारे पास एक टीटीएस सेवा है जो स्ट्रीमिंग का समर्थन करती है या कई अनुरोधों को स्वीकार करती है जो तेजी से संसाधित होते हैं?


कोई समस्या नहीं: हमें बस सर्वर और क्लाइंट साइड पर कुछ चीजों को समायोजित करने की आवश्यकता है।


सर्वर साइड पर, जब हमें AI से उत्तर का एक हिस्सा प्राप्त हो जाता है ( getAnswer फ़ंक्शन में), तो हमें अपनी TTS सेवा को कॉल करना होगा और प्रतिक्रिया के रूप में प्राप्त ऑडियो डेटा को क्लाइंट साइड पर भेजना होगा।


ग्राहक पक्ष पर और अधिक परिवर्तन की आवश्यकता है:

  • हम अब प्राप्त डेटा को टेक्स्ट में नहीं बदल सकते क्योंकि अब हम ऑडियो डेटा प्राप्त कर सकते हैं;


  • क्योंकि हो सकता है कि पिछला ऑडियो चलने से पहले ही हमें अगला ऑडियो डेटा प्राप्त हो जाए, इसलिए हमें यह ट्रैक रखने के लिए एक सिंक्रोनाइजेशन विधि शुरू करने की आवश्यकता है कि अगला कौन सा ऑडियो चलाया जाना चाहिए।

अंतिम शब्द

इस पोस्ट/कार्यान्वयन में कुछ पहलू छोड़ दिए गए हैं, जैसे कोड के विभिन्न भागों के लिए त्रुटि प्रबंधन।


यदि आप गो या रस्ट में सर्वर का कार्यान्वयन देखना चाहते हैं तो कृपया मुझे [email protected] पर लिखें।


आरेख Draw.io का उपयोग करके तैयार किए गए थे, और व्याकरण की जाँच ChatGPT द्वारा की गई थी।


संसाधन: