「GPT虛擬直播」實戰篇|GPT接入虛擬人實現直播間彈幕回復
摘要
ChatGPT和元宇宙都是當前數字化領域中非常熱門的技術和應用。結合兩者的優勢和特點,可以探索出更多的應用場景和商業模式。例如,在元宇宙中使用ChatGPT進行自然語言交互,可以為用戶提供更加智能化、個性化的服務和支持;在ChatGPT中使用元宇宙進行虛擬現實體驗,可以為用戶提供更加真實、豐富、多樣化的交互體驗。
下面我將結合元宇宙和ChatGPT的優勢,實戰開發一個GPT虛擬直播的Demo并推流到抖音平臺,
NodeJS接入ChatGPT與即構ZIM
上一篇文章《人人都能用ChatGPT4.0做Avatar虛擬人直播》,主要介紹了如何使用ChatGPT+即構Avatar做虛擬人直播。由于篇幅原因,對代碼具體實現部分描述的不夠詳細。收到不少讀者詢問代碼相關問題,接下來筆者將代碼實現部分拆分2部分來詳細描述:
- NodeJS接入ChatGPT與即構ZIM
- ChatGPT與即構Avatar虛擬人對接直播
本文主要講解如何接入ChatGPT并實現后期能與Avatar對接能力。
在開始講具體流程之前,我們先來回顧一下整個GPT虛擬直播Demo的實現流程圖,本文要分享的內容是下圖的右邊部分的實現邏輯。
1 基本原理
ChatGPT是純文本互動,那么如何讓它跟Avatar虛擬人聯系呢?
首先我們已知一個先驗:
- 即構Avatar有文本驅動能力,即給Avatar輸入一段文本,Avatar根據文本渲染口型+播報語音
- 將觀眾在直播間發送的彈幕消息抓取后,發送給OpenAI的ChatGPT服務器
- 得到ChatGPT回復后將回復內容通過Avatar語音播報
在觀眾看來,這就是在跟擁有ChatGPT一樣智商的虛擬人直播互動了。
2 本文使用的工具
- GPT虛擬直播彈幕:即構ZIM語聊房群聊彈幕
- GPT4.0:New bing
- GPT3.5:ChatGPT
3 對接ChatGPT
這里主要推薦2個庫:
- chatgpt-api
- chatgpt
chatgpt-api封裝了基于bing的chatgpt4.0,chatgpt基于openAI官方的chatgpt3.5。具體如何創建bing賬號以及如何獲取Cookie值以及如何獲取apiKey,可以參考我另一篇文章《人人都能用ChatGPT4.0做Avatar虛擬人直播》。
3.1 chatgpt-api
安裝:
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內容。
3.2 chatgpt
安裝:
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的官方接口,相對來說比較穩定,推薦這種方式使用。
3.3 兩庫一起封裝
為了更加靈活方便使用,隨意切換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函數就好!
4 對接Avatar
4.1 基本思路
好了,第2節介紹了對chatgpt的封裝,不同的版本只需調用不同函數即可實現與chatgpt對話。接下來怎么將chatGPT的文本對話內容傳遞給Avatar呢?即構Avatar是即構推出的一款虛擬形象產品,它可以跟即構內的其他產品對接,比如即時通訊ZIM和音視頻通話RTC。這就好辦了,我們只需利用ZIM或RTC即可。
這里我們主要利用即構ZIM實現,因為即構ZIM非常方便實時文本內容。即構ZIM群聊消息穩定可靠,延遲低,全球任何一個地區都有接入服務的節點保障到達。
尤其是ZIM群聊有彈幕功能,相比發送聊天消息,發送彈幕消息不會被存儲,更適合直播間評論功能。
4.2 代碼實現
即構官方提供的js版本庫主要是基于瀏覽器,需要使用到瀏覽器的特性如DOM、localStorage等。而這里我們主要基于NodeJS,沒有瀏覽器環境。因此我們需要安裝一些必要的庫, 相關庫已經在package.json有記錄,直接執行如下命令即可:
npm install
4.2.1 創建模擬瀏覽器環境
首先執行瀏覽器環境模擬,通過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;
4.2.2 創建ZIM對象
將即構官方下載的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;
}
4.2.3 登錄、創建房間、加入房間、離開房間
調用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)
});
}
4.2.4 發送消息、解析消息
發送消息分為一對一發送和發送到房間,這里通過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) {
//具體實現略
}
4.2.5 導出接口
有了以上的實現后,把關鍵函數導出暴露給其他業務調用:
module.exports = {
initZego: initZego,
sendMsg: sendMsg,
joinRoom: joinRoom
}
以上代碼主要封裝:
- 即構ZIM初始化
- 發送消息
- 加入房間
至此,我們就具備了將chatgpt消息群發到一個房間的能力、加入房間、接收到房間的彈幕消息能力。
更多關于即構ZIM接口與官方Demo可以點擊參考這里,對即構ZIM了解更多可以點擊這里
關于Avatar如何播報chatgpt內容,我們在下一篇文章實現。