「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内容,我们在下一篇文章实现。
5 相关代码
「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间弹幕回复的更多相关文章
- Python(三)基础篇之「模块&面向对象编程」
		[笔记]Python(三)基础篇之「模块&面向对象编程」 2016-12-07 ZOE 编程之魅 Python Notes: ★ 如果你是第一次阅读,推荐先浏览:[重要公告]文章更新. ... 
- 【算法数据结构专题】「延时队列算法」史上手把手教你针对层级时间轮(TimingWheel)实现延时队列的开发实战落地(上)
		承接上文 承接之前的[精华推荐 |[算法数据结构专题]「延时队列算法」史上非常详细分析和介绍如何通过时间轮(TimingWheel)实现延时队列的原理指南],让我们基本上已经知道了「时间轮算法」原理和 ... 
- DDD 实战记录——实现「借鉴学习计划」
		「借鉴学习计划」的核心是:复制一份别人的学习计划到自己的计划中,并同步推送学习任务给自己,并且每个操作都要发送通知给对方. 它们的类图如下: 它们的关系是一对多: // Schedule entity ... 
- 「查缺补漏」巩固你的Redis知识体系
		Windows Redis 安装 链接: https://pan.baidu.com/s/1MJnzX_qRuNXJI09euzkPGA 提取码: 2c6w 复制这段内容后打开百度网盘手机App,操作 ... 
- 持续集成之 Spring Boot 实战篇
		本文作者: CODING 用户 - 何健 这次实战篇,我们借助「CODING 持续集成」,实现一个简单的 Spring Boot 项目从编码到最后部署的完整过程.本教程还有 B 站视频版,帮助读者更好 ... 
- [译]聊聊C#中的泛型的使用(新手勿入)  Seaching TreeVIew WPF  可编辑树Ztree的使用(包括对后台数据库的增删改查)  字段和属性的区别  C# 遍历Dictionary并修改其中的Value  学习笔记——异步  程序员常说的「哈希表」是个什么鬼?
		[译]聊聊C#中的泛型的使用(新手勿入) 写在前面 今天忙里偷闲在浏览外文的时候看到一篇讲C#中泛型的使用的文章,因此加上本人的理解以及四级没过的英语水平斗胆给大伙进行了翻译,当然在翻译的过程中发 ... 
- 手牵手,使用uni-app从零开发一款视频小程序 (系列下 开发实战篇)
		系列文章 手牵手,使用uni-app从零开发一款视频小程序 (系列上 准备工作篇) 手牵手,使用uni-app从零开发一款视频小程序 (系列下 开发实战篇) 扫码体验,先睹为快 可以扫描下微信小程序的 ... 
- 「查缺补漏」巩固你的Nginx知识体系
		Nginx篇 基本介绍 Nginx是一款轻量级的 Web服务器 / 反向代理服务器 / 电子邮件(IMAP/POP3)代理服务器,主要的优点是: 支持高并发连接,尤其是静态界面,官方测试Nginx能够 ... 
- 「每日一题」有人上次在dy面试,面试官问我:vue数据绑定的实现原理。你说我该如何回答?
		关注「松宝写代码」,精选好文,每日一题 时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 来源:原创 一.前言 文章首发在「松宝写代码」 2020. ... 
- B 站崩了,总结下「高可用」和「异地多活」
		你好,我是悟空. 一.背景 不用想象一种异常场景了,这就真实发生了:B 站晚上 11 点突然挂了,网站主页直接报 404. 手机 APP 端数据加载不出来. 23:30 分,B 站做了降级页面,将 4 ... 
随机推荐
- Docker教程、架构、资源
			一.Docker教程  Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源.Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中 ... 
- SpringBoot——配置及原理
			更多内容,前往IT-BLOG 一.Spring Boot全局配置文件 application.properties 与 application.yml 配置文件的作用:可以覆盖 SpringBoot ... 
- Spring Data Redis 框架
			系统性学习,移步IT-BLOG 一.简介 对于类似于首页这种每天都有大量的人访问,对数据库造成很大的压力,严重时可能导致瘫痪.解决方法:一种是数据缓存.一种是网页静态化.今天就讨论数据缓存的实现 Re ... 
- Windows10 穿越火线手感和Windows7不一样
			如果是穿越火线或者其他FPS玩家,应该会感觉Win10和WIin7两者手感会有一定的区别.为什么升级了系统变菜了?心理作用?其实确实和系统有关系哦.我从Windows7升级到Windows10玩穿越火 ... 
- NotionAI - 文档领域的ChatGPT,一款 AI 加持的在线文档编辑和管理工具
			简介 NotionAI - 文档领域的ChatGPT,一款 AI 加持的在线文档编辑和管理工具 作为国际领先的在线文档编辑和管理工具,Notion受到了广大用户的欢迎,尤其是程序员们.它不仅支持笔记. ... 
- 在 Rainbond 上使用 Curve 云原生存储
			Curve 是网易主导自研的现代化存储系统, 目前支持文件存储(CurveFS)和块存储(CurveBS). CurveBS 的核心应用场景主要包括: 虚拟机/容器的性能型.混合型.容量型云盘或持久化 ... 
- opengl helloworld vscode 通过glfw 绘制三角形
			opengl helloworld vscode 调用glfw 绘制三角形 目录 opengl helloworld vscode 调用glfw 绘制三角形 打开 glfw.org, 我下的64 目录 ... 
- Notion AI:门槛更低的ChatGPT Plus
			[2023年3月27日]由于接口成本的问题,如今的大部分应用应该都只会建立在GPT-3/ChatGPT接口的基础上,所以想要体验GPT-4,还是得尊贵的ChatGPT Plus. 前段日子体验了Not ... 
- 用Abp实现两步验证(Two-Factor Authentication,2FA)登录(二):Vue网页端开发
			@ 目录 发送验证码 登录 退出登录 界面控件 获取用户信息功能 项目地址 前端代码的框架采用vue.js + elementUI 这套较为简单的方式实现,以及typescript语法更方便阅读. 首 ... 
- vivo全球商城:电商交易平台设计
			作者:vivo 官网商城开发团队 - Cheng Kun.Liu Wei 本文介绍了交易平台的设计理念和关键技术方案,以及实践过程中的思考与挑战. 点击查阅:<vivo 全球商城>系列文章 ... 
