共计 9715 个字符,预计需要花费 25 分钟才能阅读完成。
摘要
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 内容,咱们在下一篇文章实现。
5 相干代码
- nodejs 接入 chatgpt 与即构 zim