• 「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间


    摘要

    ChatGPT和元宇宙都是当前数字化领域中非常热门的技术和应用。结合两者的优势和特点,可以探索出更多的应用场景和商业模式。例如,在元宇宙中使用ChatGPT进行自然语言交互,可以为用户提供更加智能化、个性化的服务和支持;在ChatGPT中使用元宇宙进行虚拟现实体验,可以为用户提供更加真实、丰富、多样化的交互体验。
    下面我将结合元宇宙和ChatGPT的优势,实战开发一个GPT虚拟直播的Demo并推流到抖音平台,

    NodeJS接入ChatGPT与即构ZIM

    接下来笔者将代码实现部分拆分2部分来详细描述:

    1. NodeJS接入ChatGPT与即构ZIM
    2. ChatGPT与即构Avatar虚拟人对接直播

    本文主要讲解如何接入ChatGPT并实现后期能与Avatar对接能力。

    在开始讲具体流程之前,我们先来回顾一下整个GPT虚拟直播Demo的实现流程图,本文要分享的内容是下图的右边部分的实现逻辑。

    1 基本原理

    ChatGPT是纯文本互动,那么如何让它跟Avatar虚拟人联系呢?
    首先我们已知一个先验:

    • 即构Avatar有文本驱动能力,即给Avatar输入一段文本,Avatar根据文本渲染口型+播报语音
    • 将观众在直播间发送的弹幕消息抓取后,发送给OpenAI的ChatGPT服务器
    • 得到ChatGPT回复后将回复内容通过Avatar语音播报
      在观众看来,这就是在跟拥有ChatGPT一样智商的虚拟人直播互动了。

    2 本文使用的工具

    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,因此需要一个代理,因此需要把代理地址也一起封装。代码如下:

    1. import { BingAIClient } from '@waylaidwanderer/chatgpt-api';
    2. export class BingGPT {
    3. /*
    4. * http_proxy, apiKey
    5. **/
    6. constructor(http_proxy, userCookie) {
    7. this.api = this.init(http_proxy, userCookie);
    8. this.conversationSignature = "";
    9. this.conversationId = "";
    10. this.clientId = "";
    11. this.invocationId = "";
    12. }
    13. init(http_proxy, userCookie) {
    14. console.log(http_proxy, userCookie)
    15. const options = {
    16. host: 'https://www.bing.com',
    17. userToken: userCookie,
    18. // If the above doesn't work, provide all your cookies as a string instead
    19. cookies: '',
    20. // A proxy string like "http://:"
    21. proxy: http_proxy,
    22. // (Optional) Set to true to enable `console.debug()` logging
    23. debug: false,
    24. };
    25. return new BingAIClient(options);
    26. }
    27. //
    28. //此处省略chat函数......
    29. //
    30. }

    上面代码完成了VPN和BingAIClient的封装,还缺少聊天接口,因此添加chat函数完成聊天功能:

    1. //调用chatpgt
    2. chat(text, cb) {
    3. var res=""
    4. var that = this;
    5. console.log("正在向bing发送提问", text )
    6. this.api.sendMessage(text, {
    7. toneStyle: 'balanced',
    8. onProgress: (token) => {
    9. if(token.length==2 && token.charCodeAt(0)==55357&&token.charCodeAt(1)==56842){
    10. cb(true, res);
    11. }
    12. res+=token;
    13. }
    14. }).then(function(response){
    15. that.conversationSignature = response.conversationSignature;
    16. that.conversationId = response.conversationId;
    17. that.clientId = response.clientId;
    18. that.invocationId = response.invocationId;
    19. }) ;
    20. }

    在使用的时候只需如下调用:

    1. var bing = new BingGPT(HTTP_PROXY, BING_USER_COOKIE);
    2. bing.chat("这里传入提问内容XXXX?", function(succ, response){
    3. if(succ)
    4. console.log("回复内容:", response)
    5. })

    需要注意的是,基于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库一起集成封装成一个类:

    1. import { ChatGPTAPI } from "chatgpt";
    2. import proxy from "https-proxy-agent";
    3. import nodeFetch from "node-fetch";
    4. export class ChatGPT {
    5. constructor(http_proxy, apiKey) {
    6. this.api = this.init(http_proxy, apiKey);
    7. this.conversationId = null;
    8. this.ParentMessageId = null;
    9. }
    10. init(http_proxy, apiKey) {
    11. console.log(http_proxy, apiKey)
    12. return new ChatGPTAPI({
    13. apiKey: apiKey,
    14. fetch: (url, options = {}) => {
    15. const defaultOptions = {
    16. agent: proxy(http_proxy),
    17. };
    18. const mergedOptions = {
    19. ...defaultOptions,
    20. ...options,
    21. };
    22. return nodeFetch(url, mergedOptions);
    23. },
    24. });
    25. }
    26. //...
    27. //此处省略chat函数
    28. //...
    29. }

    完成ChatGPTAPI的封装后,接下来添加聊天接口:

    1. //调用chatpgt
    2. chat(text, cb) {
    3. let that = this
    4. console.log("正在向ChatGPT发送提问:", text)
    5. that.api.sendMessage(text, {
    6. conversationId: that.ConversationId,
    7. parentMessageId: that.ParentMessageId
    8. }).then(
    9. function (res) {
    10. that.ConversationId = res.conversationId
    11. that.ParentMessageId = res.id
    12. cb && cb(true, res.text)
    13. }
    14. ).catch(function (err) {
    15. console.log(err)
    16. cb && cb(false, err);
    17. });
    18. }

    使用时就非常简单:

    1. var chatgpt = new ChatGPT(HTTP_PROXY, API_KEY);
    2. chatgpt.chat("这里传入提问内容XXXX?", function(succ, response){
    3. if(succ)
    4. console.log("回复内容:", response)
    5. })

    chatgpt库主要基于openAI的官方接口,相对来说比较稳定,推荐这种方式使用。

    3.3 两库一起封装

    为了更加灵活方便使用,随意切换chatgpt3.5和chatgpt4.0。将以上两个库封装到一个接口中。

    首先创建一个文件保存各种配置, KeyCenter.js:

    1. const HTTP_PROXY = "http://127.0.0.1:xxxx";//本地vpn代理端口
    2. //openAI的key,
    3. const API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxx";
    4. //bing cookie
    5. const BING_USER_COOKIE = 'xxxxxxxxxxxxxxxxxxxxxxxx--BA';
    6. module.exports = {
    7. HTTP_PROXY: HTTP_PROXY,
    8. API_KEY: API_KEY,
    9. BING_USER_COOKIE:BING_USER_COOKIE
    10. }

    注意,以上相关配置内容需要读者替换。

    接下来封装两个不同版本的chatGPT:

    1. const KEY_CENTER = require("../KeyCenter.js");
    2. var ChatGPTObj = null, BingGPTObj = null;
    3. //初始化chatgpt
    4. function getChatGPT(onInitedCb) {
    5. if (ChatGPTObj != null) {
    6. onInitedCb(true, ChatGPTObj);
    7. return;
    8. }
    9. (async () => {
    10. let { ChatGPT } = await import("./chatgpt.mjs");
    11. return new ChatGPT(KEY_CENTER.HTTP_PROXY, KEY_CENTER.API_KEY);
    12. })().then(function (obj) {
    13. ChatGPTObj = obj;
    14. onInitedCb(true, obj);
    15. }).catch(function (err) {
    16. onInitedCb(false, err);
    17. });
    18. }
    19. function getBingGPT(onInitedCb){
    20. if(BingGPTObj!=null) {
    21. onInitedCb(true, BingGPTObj);
    22. return;
    23. }
    24. (async () => {
    25. let { BingGPT } = await import("./binggpt.mjs");
    26. return new BingGPT(KEY_CENTER.HTTP_PROXY, KEY_CENTER.BING_USER_COOKIE);
    27. })().then(function (obj) {
    28. BingGPTObj = obj;
    29. onInitedCb(true, obj);
    30. }).catch(function (err) {
    31. console.log(err)
    32. onInitedCb(false, err);
    33. });
    34. }

    上面两个函数getBingGPTgetChatGPT分别对应2.1节2.2节封装的版本。在切换版本的时候直接调用对应的函数即可,但笔者认为,还不够优雅!使用起来还是不够舒服,因为需要维护不同的对象。最好能进一步封装,调用的时候一行代码来使用是最好的。那进一步封装,补充以下代码:

    1. //调用chatgpt聊天
    2. function chatGPT(text, cb) {
    3. getChatGPT(function (succ, obj) {
    4. if (succ) {
    5. obj.chat(text, cb);
    6. } else {
    7. cb && cb(false, "chatgpt not inited!!!");
    8. }
    9. })
    10. }
    11. function chatBing(text, cb){
    12. getBingGPT(function (succ, obj) {
    13. if (succ) {
    14. obj.chat(text, cb);
    15. } else {
    16. cb && cb(false, "chatgpt not inited!!!");
    17. }
    18. })
    19. }
    20. module.exports = {
    21. chatGPT: chatGPT,
    22. chatBing:chatBing
    23. }

    加了以上代码后,就舒服多了:想要使用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等全局对象。

    1. var fs = require('fs');
    2. //先清理缓存
    3. fs.readdirSync('./local_storage').forEach(function (fileName) {
    4. fs.unlinkSync('./local_storage/' + fileName);
    5. });
    6. const KEY_CENTER = require("../KeyCenter.js");
    7. const APPID = KEY_CENTER.APPID, SERVER_SECRET = KEY_CENTER.SERVER_SECRET;
    8. const generateToken04 = require('./TokenUtils.js').generateToken04;
    9. var LocalStorage = require('node-localstorage').LocalStorage;
    10. localStorage = new LocalStorage('./local_storage');
    11. var indexedDB = require("fake-indexeddb/auto").indexedDB;
    12. const jsdom = require("jsdom");
    13. const { JSDOM } = jsdom;
    14. const dom = new JSDOM(``, {
    15. url: "http://localhost/",
    16. referrer: "http://localhost/",
    17. contentType: "text/html",
    18. includeNodeLocations: true,
    19. storageQuota: 10000000
    20. });
    21. window = dom.window;
    22. document = window.document;
    23. navigator = window.navigator;
    24. location = window.location;
    25. WebSocket = window.WebSocket;
    26. XMLHttpRequest = window.XMLHttpRequest;

    4.2.2 创建ZIM对象

    将即构官方下载的index.js引入,获取ZIM类并实例化,这个过程封装到createZIM函数中。需要注意的是登录需要Token,为了安全考虑,Token建议在服务器端生成。接下来把整个初始化过程封装到initZego函数中,包含注册监听接收消息,监控Token过期并重置。

    1. const ZIM = require('./index.js').ZIM;
    2. function newToken(userId) {
    3. const token = generateToken04(APPID, userId, SERVER_SECRET, 60 * 60 * 24, '');
    4. return token;
    5. }
    6. /**
    7. * 创建ZIM对象
    8. */
    9. function createZIM(onError, onRcvMsg, onTokenWillExpire) {
    10. var zim = ZIM.create(APPID);
    11. zim.on('error', onError);
    12. zim.on('receivePeerMessage', function (zim, msgObj) {
    13. console.log("收到P2P消息")
    14. onRcvMsg(false, zim, msgObj)
    15. });
    16. // 收到群组消息的回调
    17. zim.on('receiveRoomMessage', function (zim, msgObj) {
    18. console.log("收到群组消息")
    19. onRcvMsg(true, zim, msgObj)
    20. });
    21. zim.on('tokenWillExpire', onTokenWillExpire);
    22. return zim;
    23. }
    24. /*
    25. *初始化即构ZIM
    26. */
    27. function initZego(onError, onRcvMsg, myUID) {
    28. var token = newToken(myUID);
    29. var startTimestamp = new Date().getTime();
    30. function _onError(zim, err) {
    31. onError(err);
    32. }
    33. function _onRcvMsg(isFromGroup, zim, msgObj) {
    34. var msgList = msgObj.messageList;
    35. var fromConversationID = msgObj.fromConversationID;
    36. msgList.forEach(function (msg) {
    37. if (msg.timestamp - startTimestamp >= 0) { //过滤掉离线消息
    38. var out = parseMsg(zim, isFromGroup, msg.message, fromConversationID)
    39. if (out)
    40. onRcvMsg(out);
    41. }
    42. })
    43. }
    44. function onTokenWillExpire(zim, second) {
    45. token = newToken(userId);
    46. zim.renewToken(token);
    47. }
    48. var zim = createZIM(_onError, _onRcvMsg, onTokenWillExpire);
    49. login(zim, myUID, token, function (succ, data) {
    50. if (succ) {
    51. console.log("登录成功!")
    52. } else {
    53. console.log("登录失败!", data)
    54. }
    55. })
    56. return zim;
    57. }

    4.2.3 登录、创建房间、加入房间、离开房间

    调用zim对象的login函数完成登录,封装到login函数中;调用zim对象的joinRoom完成加入房间,封装到joinRoom函数中;调用zim的leaveRoom函数完成退出房间,封装到leaveRoom函数中。

    1. /**
    2. * 登录即构ZIM
    3. */
    4. function login(zim, userId, token, cb) {
    5. var userInfo = { userID: userId, userName: userId };
    6. zim.login(userInfo, token)
    7. .then(function () {
    8. cb(true, null);
    9. })
    10. .catch(function (err) {
    11. cb(false, err);
    12. });
    13. }
    14. /**
    15. * 加入房间
    16. */
    17. function joinRoom(zim, roomId, cb = null) {
    18. zim.joinRoom(roomId)
    19. .then(function ({ roomInfo }) {
    20. cb && cb(true, roomInfo);
    21. })
    22. .catch(function (err) {
    23. cb && cb(false, err);
    24. });
    25. }
    26. /**
    27. * 离开房间
    28. */
    29. function leaveRoom(zim, roomId) {
    30. zim.leaveRoom(roomId)
    31. .then(function ({ roomID }) {
    32. // 操作成功
    33. console.log("已离开房间", roomID)
    34. })
    35. .catch(function (err) {
    36. // 操作失败
    37. console.log("离开房间失败", err)
    38. });
    39. }

    4.2.4 发送消息、解析消息

    发送消息分为一对一发送和发送到房间,这里通过isGroup参数来控制,如下sendMsg函数所示。将接收消息UID和发送内容作为sendMsg参数,最终封装并调用ZIM的sendMessage函数完成消息发送。

    接收到消息后,在我们的应用中设置了发送的消息内容是个json对象,因此需要对内容进行解析,具体的json格式可以参考完整源码,这里不做详细讲解。

    1. /**
    2. * 发送消息
    3. */
    4. function sendMsg(zim, isGroup, msg, toUID, cb) {
    5. var type = isGroup ? 1 : 0; // 会话类型,取值为 单聊:0,房间:1,群组:2
    6. var config = {
    7. priority: 1, // 设置消息优先级,取值为 低:1(默认),中:2,高:3
    8. };
    9. var messageTextObj = { type: 20, message: msg, extendedData: '' };
    10. var notification = {
    11. onMessageAttached: function (message) {
    12. console.log("已发送", message)
    13. }
    14. }
    15. zim.sendMessage(messageTextObj, toUID, type, config, notification)
    16. .then(function ({ message }) {
    17. // 发送成功
    18. cb(true, null);
    19. })
    20. .catch(function (err) {
    21. // 发送失败
    22. cb(false, err)
    23. });
    24. }
    25. /**
    26. * 解析收到的消息
    27. */
    28. function parseMsg(zim, isFromGroup, msg, fromUid) {
    29. //具体实现略
    30. }

    4.2.5 导出接口

    有了以上的实现后,把关键函数导出暴露给其他业务调用:

    1. module.exports = {
    2. initZego: initZego,
    3. sendMsg: sendMsg,
    4. joinRoom: joinRoom
    5. }

    以上代码主要封装:

    1. 即构ZIM初始化
    2. 发送消息
    3. 加入房间

    至此,我们就具备了将chatgpt消息群发到一个房间的能力、加入房间、接收到房间的弹幕消息能力。

    更多关于即构ZIM接口与官方Demo可以点击参考这里,对即构ZIM了解更多可以点击这里

    关于Avatar如何播报chatgpt内容,我们在下一篇文章实现。

    5 相关代码

    1. nodejs接入chatgpt与即构zim
  • 相关阅读:
    这可能是2022年把微服务讲的最全了:SpringBoot+Cloud+Docker
    「教师资格证定期注册」相关答疑
    【FusionInsight 迁移】HBase从C50迁移到6.5.1(02)C50上准备FTP Server
    npm install 报错 chromedriver 安装失败的解决办法
    邱锡鹏神经网络怎么样,邱锡鹏神经网络答案
    通过redis在控制台模拟手机验证码功能(java)
    java学习--day12(抽象类与接口作业)
    springboot大学生就业招聘网站java ssm
    SpringBoot 面试题总结 (JavaGuide)
    vue3在子组件中触发的父组件函数方式
  • 原文地址:https://blog.csdn.net/2301_77550592/article/details/133043281