9

「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间弹幕回复 - 程序员_Rya

 11 months ago
source link: https://www.cnblogs.com/RTCWang/p/17431941.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

NodeJS接入ChatGPT与即构ZIM

上一篇文章《人人都能用ChatGPT4.0做Avatar虚拟人直播》,主要介绍了如何使用ChatGPT+即构Avatar做虚拟人直播。由于篇幅原因,对代码具体实现部分描述的不够详细。收到不少读者询问代码相关问题,接下来笔者将代码实现部分拆分2部分来详细描述:

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

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

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

2949130-20230525170628627-1353923302.jpg

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


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);
    });
}

上面两个函数getBingGPTgetChatGPT分别对应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
}

以上代码主要封装:

  1. 即构ZIM初始化

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

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

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

5 相关代码

  1. nodejs接入chatgpt与即构zim

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK