ChatGPT和元宇宙都是當前數位化領域中非常熱門的技術和應用。結合兩者的優勢和特點,可以探索出更多的應用場景和商業模式。例如,在元宇宙中使用ChatGPT進行自然語言互動,可以為使用者提供更加智慧化、個性化的服務和支援;在ChatGPT中使用元宇宙進行虛擬現實體驗,可以為使用者提供更加真實、豐富、多樣化的互動體驗。
下面我將結合元宇宙和ChatGPT的優勢,實戰開發一個GPT虛擬直播的Demo並推流到抖音平臺,
上一篇文章《人人都能用ChatGPT4.0做Avatar虛擬人直播》,主要介紹瞭如何使用ChatGPT+即構Avatar做虛擬人直播。由於篇幅原因,對程式碼具體實現部分描述的不夠詳細。收到不少讀者詢問程式碼相關問題,接下來筆者將程式碼實現部分拆分2部分來詳細描述:
本文主要講解如何接入ChatGPT並實現後期能與Avatar對接能力。
在開始講具體流程之前,我們先來回顧一下整個GPT虛擬直播Demo的實現流程圖,本文要分享的內容是下圖的右邊部分的實現邏輯。
ChatGPT是純文字互動,那麼如何讓它跟Avatar虛擬人聯絡呢?
首先我們已知一個先驗:
這裡主要推薦2個庫:
chatgpt-api封裝了基於bing的chatgpt4.0,chatgpt基於openAI官方的chatgpt3.5。具體如何建立bing賬號以及如何獲取Cookie值以及如何獲取apiKey,可以參考我另一篇文章《人人都能用ChatGPT4.0做Avatar虛擬人直播》。
安裝:
npm i @waylaidwanderer/chatgpt-api
bing還沒有對中國大陸開放chatgpt,因此需要一個代理,因此需要把代理地址也一起封裝。程式碼如下:
import { BingAIClient } from '@waylaidwanderer/chatgpt-api';
export class BingGPT {
/*
* http_proxy, apiKey
**/
constructor(http_proxy, userCookie) {
this.api = this.init(http_proxy, userCookie);
this.conversationSignature = "";
this.conversationId = "";
this.clientId = "";
this.invocationId = "";
}
init(http_proxy, userCookie) {
console.log(http_proxy, userCookie)
const options = {
host: 'https://www.bing.com',
userToken: userCookie,
// If the above doesn't work, provide all your cookies as a string instead
cookies: '',
// A proxy string like "http://<ip>:<port>"
proxy: http_proxy,
// (Optional) Set to true to enable `console.debug()` logging
debug: false,
};
return new BingAIClient(options);
}
//
//此處省略chat函數......
//
}
上面程式碼完成了VPN和BingAIClient的封裝,還缺少聊天介面,因此新增chat函數完成聊天功能:
//呼叫chatpgt
chat(text, cb) {
var res=""
var that = this;
console.log("正在向bing傳送提問", text )
this.api.sendMessage(text, {
toneStyle: 'balanced',
onProgress: (token) => {
if(token.length==2 && token.charCodeAt(0)==55357&&token.charCodeAt(1)==56842){
cb(true, res);
}
res+=token;
}
}).then(function(response){
that.conversationSignature = response.conversationSignature;
that.conversationId = response.conversationId;
that.clientId = response.clientId;
that.invocationId = response.invocationId;
}) ;
}
在使用的時候只需如下呼叫:
var bing = new BingGPT(HTTP_PROXY, BING_USER_COOKIE);
bing.chat("這裡傳入提問內容XXXX?", function(succ, response){
if(succ)
console.log("回覆內容:", response)
})
需要注意的是,基於bing的chatgpt4.0主要是通過模擬瀏覽器方式封住。在瀏覽器端有很多防機器人檢測,因此容易被卡斷。這裡筆者建議僅限自己體驗,不適合作為產品介面使用。如果需要封裝成產品,建議使用下一節2.2內容。
安裝:
npm install chatgpt
跟上一小節2.1類似,基於openAI的chatgpt3.5依舊需要梯子才能使用。chatgpt庫沒有內建代理能力,因此我們可以自己安裝代理庫:
npm install https-proxy-agent node-fetch
接下來將代理和chatgpt庫一起整合封裝成一個類:
import { ChatGPTAPI } from "chatgpt";
import proxy from "https-proxy-agent";
import nodeFetch from "node-fetch";
export class ChatGPT {
constructor(http_proxy, apiKey) {
this.api = this.init(http_proxy, apiKey);
this.conversationId = null;
this.ParentMessageId = null;
}
init(http_proxy, apiKey) {
console.log(http_proxy, apiKey)
return new ChatGPTAPI({
apiKey: apiKey,
fetch: (url, options = {}) => {
const defaultOptions = {
agent: proxy(http_proxy),
};
const mergedOptions = {
...defaultOptions,
...options,
};
return nodeFetch(url, mergedOptions);
},
});
}
//...
//此處省略chat函數
//...
}
完成ChatGPTAPI的封裝後,接下來新增聊天介面:
//呼叫chatpgt
chat(text, cb) {
let that = this
console.log("正在向ChatGPT傳送提問:", text)
that.api.sendMessage(text, {
conversationId: that.ConversationId,
parentMessageId: that.ParentMessageId
}).then(
function (res) {
that.ConversationId = res.conversationId
that.ParentMessageId = res.id
cb && cb(true, res.text)
}
).catch(function (err) {
console.log(err)
cb && cb(false, err);
});
}
使用時就非常簡單:
var chatgpt = new ChatGPT(HTTP_PROXY, API_KEY);
chatgpt.chat("這裡傳入提問內容XXXX?", function(succ, response){
if(succ)
console.log("回覆內容:", response)
})
chatgpt庫主要基於openAI的官方介面,相對來說比較穩定,推薦這種方式使用。
為了更加靈活方便使用,隨意切換chatgpt3.5和chatgpt4.0。將以上兩個庫封裝到一個介面中。
首先建立一個檔案儲存各種設定, KeyCenter.js:
const HTTP_PROXY = "http://127.0.0.1:xxxx";//本地vpn代理埠
//openAI的key,
const API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxx";
//bing cookie
const BING_USER_COOKIE = 'xxxxxxxxxxxxxxxxxxxxxxxx--BA';
module.exports = {
HTTP_PROXY: HTTP_PROXY,
API_KEY: API_KEY,
BING_USER_COOKIE:BING_USER_COOKIE
}
注意,以上相關設定內容需要讀者替換。
接下來封裝兩個不同版本的chatGPT:
const KEY_CENTER = require("../KeyCenter.js");
var ChatGPTObj = null, BingGPTObj = null;
//初始化chatgpt
function getChatGPT(onInitedCb) {
if (ChatGPTObj != null) {
onInitedCb(true, ChatGPTObj);
return;
}
(async () => {
let { ChatGPT } = await import("./chatgpt.mjs");
return new ChatGPT(KEY_CENTER.HTTP_PROXY, KEY_CENTER.API_KEY);
})().then(function (obj) {
ChatGPTObj = obj;
onInitedCb(true, obj);
}).catch(function (err) {
onInitedCb(false, err);
});
}
function getBingGPT(onInitedCb){
if(BingGPTObj!=null) {
onInitedCb(true, BingGPTObj);
return;
}
(async () => {
let { BingGPT } = await import("./binggpt.mjs");
return new BingGPT(KEY_CENTER.HTTP_PROXY, KEY_CENTER.BING_USER_COOKIE);
})().then(function (obj) {
BingGPTObj = obj;
onInitedCb(true, obj);
}).catch(function (err) {
console.log(err)
onInitedCb(false, err);
});
}
上面兩個函數getBingGPT
和getChatGPT
分別對應2.1節
和2.2節
封裝的版本。在切換版本的時候直接呼叫對應的函數即可,但筆者認為,還不夠優雅!使用起來還是不夠舒服,因為需要維護不同的物件。最好能進一步封裝,呼叫的時候一行程式碼來使用是最好的。那進一步封裝,補充以下程式碼:
//呼叫chatgpt聊天
function chatGPT(text, cb) {
getChatGPT(function (succ, obj) {
if (succ) {
obj.chat(text, cb);
} else {
cb && cb(false, "chatgpt not inited!!!");
}
})
}
function chatBing(text, cb){
getBingGPT(function (succ, obj) {
if (succ) {
obj.chat(text, cb);
} else {
cb && cb(false, "chatgpt not inited!!!");
}
})
}
module.exports = {
chatGPT: chatGPT,
chatBing:chatBing
}
加了以上程式碼後,就舒服多了:想要使用bing的chatgpt4.0,那就呼叫chatBing函數好了;想要使用openAI官方的chatgpt3.5,那就呼叫chatGPT函數就好!
好了,第2節介紹了對chatgpt的封裝,不同的版本只需呼叫不同函數即可實現與chatgpt對話。接下來怎麼將chatGPT的文字對話內容傳遞給Avatar呢?即構Avatar是即構推出的一款虛擬形象產品,它可以跟即構內的其他產品對接,比如即時通訊ZIM和音視訊通話RTC。這就好辦了,我們只需利用ZIM或RTC即可。
這裡我們主要利用即構ZIM實現,因為即構ZIM非常方便實時文字內容。即構ZIM群聊訊息穩定可靠,延遲低,全球任何一個地區都有接入服務的節點保障到達。
尤其是ZIM群聊有彈幕功能,相比傳送聊天訊息,傳送彈幕訊息不會被儲存,更適合直播間評論功能。
即構官方提供的js版本庫主要是基於瀏覽器,需要使用到瀏覽器的特性如DOM、localStorage等。而這裡我們主要基於NodeJS,沒有瀏覽器環境。因此我們需要安裝一些必要的庫, 相關庫已經在package.json有記錄,直接執行如下命令即可:
npm install
首先執行瀏覽器環境模擬,通過fake-indexeddb、jsdom、node-localstorage庫模擬瀏覽器環境以及本地儲存環境。建立WebSocket、XMLHttpRequest等全域性物件。
var fs = require('fs');
//先清除快取
fs.readdirSync('./local_storage').forEach(function (fileName) {
fs.unlinkSync('./local_storage/' + fileName);
});
const KEY_CENTER = require("../KeyCenter.js");
const APPID = KEY_CENTER.APPID, SERVER_SECRET = KEY_CENTER.SERVER_SECRET;
const generateToken04 = require('./TokenUtils.js').generateToken04;
var LocalStorage = require('node-localstorage').LocalStorage;
localStorage = new LocalStorage('./local_storage');
var indexedDB = require("fake-indexeddb/auto").indexedDB;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(``, {
url: "http://localhost/",
referrer: "http://localhost/",
contentType: "text/html",
includeNodeLocations: true,
storageQuota: 10000000
});
window = dom.window;
document = window.document;
navigator = window.navigator;
location = window.location;
WebSocket = window.WebSocket;
XMLHttpRequest = window.XMLHttpRequest;
將即構官方下載的index.js引入,獲取ZIM類並範例化,這個過程封裝到createZIM函數中。需要注意的是登入需要Token,為了安全考慮,Token建議在伺服器端生成。接下來把整個初始化過程封裝到initZego函數中,包含註冊監聽接收訊息,監控Token過期並重置。
const ZIM = require('./index.js').ZIM;
function newToken(userId) {
const token = generateToken04(APPID, userId, SERVER_SECRET, 60 * 60 * 24, '');
return token;
}
/**
* 建立ZIM物件
*/
function createZIM(onError, onRcvMsg, onTokenWillExpire) {
var zim = ZIM.create(APPID);
zim.on('error', onError);
zim.on('receivePeerMessage', function (zim, msgObj) {
console.log("收到P2P訊息")
onRcvMsg(false, zim, msgObj)
});
// 收到群組訊息的回撥
zim.on('receiveRoomMessage', function (zim, msgObj) {
console.log("收到群組訊息")
onRcvMsg(true, zim, msgObj)
});
zim.on('tokenWillExpire', onTokenWillExpire);
return zim;
}
/*
*初始化即構ZIM
*/
function initZego(onError, onRcvMsg, myUID) {
var token = newToken(myUID);
var startTimestamp = new Date().getTime();
function _onError(zim, err) {
onError(err);
}
function _onRcvMsg(isFromGroup, zim, msgObj) {
var msgList = msgObj.messageList;
var fromConversationID = msgObj.fromConversationID;
msgList.forEach(function (msg) {
if (msg.timestamp - startTimestamp >= 0) { //過濾掉離線訊息
var out = parseMsg(zim, isFromGroup, msg.message, fromConversationID)
if (out)
onRcvMsg(out);
}
})
}
function onTokenWillExpire(zim, second) {
token = newToken(userId);
zim.renewToken(token);
}
var zim = createZIM(_onError, _onRcvMsg, onTokenWillExpire);
login(zim, myUID, token, function (succ, data) {
if (succ) {
console.log("登入成功!")
} else {
console.log("登入失敗!", data)
}
})
return zim;
}
呼叫zim物件的login函數完成登入,封裝到login函數中;呼叫zim物件的joinRoom完成加入房間,封裝到joinRoom函數中;呼叫zim的leaveRoom函數完成退出房間,封裝到leaveRoom函數中。
/**
* 登入即構ZIM
*/
function login(zim, userId, token, cb) {
var userInfo = { userID: userId, userName: userId };
zim.login(userInfo, token)
.then(function () {
cb(true, null);
})
.catch(function (err) {
cb(false, err);
});
}
/**
* 加入房間
*/
function joinRoom(zim, roomId, cb = null) {
zim.joinRoom(roomId)
.then(function ({ roomInfo }) {
cb && cb(true, roomInfo);
})
.catch(function (err) {
cb && cb(false, err);
});
}
/**
* 離開房間
*/
function leaveRoom(zim, roomId) {
zim.leaveRoom(roomId)
.then(function ({ roomID }) {
// 操作成功
console.log("已離開房間", roomID)
})
.catch(function (err) {
// 操作失敗
console.log("離開房間失敗", err)
});
}
傳送訊息分為一對一傳送和傳送到房間,這裡通過isGroup引數來控制,如下sendMsg函數所示。將接收訊息UID和傳送內容作為sendMsg引數,最終封裝並呼叫ZIM的sendMessage函數完成訊息傳送。
接收到訊息後,在我們的應用中設定了傳送的訊息內容是個json物件,因此需要對內容進行解析,具體的json格式可以參考完整原始碼,這裡不做詳細講解。
/**
* 傳送訊息
*/
function sendMsg(zim, isGroup, msg, toUID, cb) {
var type = isGroup ? 1 : 0; // 對談型別,取值為 單聊:0,房間:1,群組:2
var config = {
priority: 1, // 設定訊息優先順序,取值為 低:1(預設),中:2,高:3
};
var messageTextObj = { type: 20, message: msg, extendedData: '' };
var notification = {
onMessageAttached: function (message) {
console.log("已傳送", message)
}
}
zim.sendMessage(messageTextObj, toUID, type, config, notification)
.then(function ({ message }) {
// 傳送成功
cb(true, null);
})
.catch(function (err) {
// 傳送失敗
cb(false, err)
});
}
/**
* 解析收到的訊息
*/
function parseMsg(zim, isFromGroup, msg, fromUid) {
//具體實現略
}
有了以上的實現後,把關鍵函數匯出暴露給其他業務呼叫:
module.exports = {
initZego: initZego,
sendMsg: sendMsg,
joinRoom: joinRoom
}
以上程式碼主要封裝:
至此,我們就具備了將chatgpt訊息群發到一個房間的能力、加入房間、接收到房間的彈幕訊息能力。
更多關於即構ZIM介面與官方Demo可以點選參考這裡,對即構ZIM瞭解更多可以點選這裡
關於Avatar如何播報chatgpt內容,我們在下一篇文章實現。