「GPT虛擬直播」實戰篇|GPT接入虛擬人實現直播間彈幕回覆

2023-05-26 12:02:13

摘要

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的實現流程圖,本文要分享的內容是下圖的右邊部分的實現邏輯。

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初始化
  2. 傳送訊息
  3. 加入房間

至此,我們就具備了將chatgpt訊息群發到一個房間的能力、加入房間、接收到房間的彈幕訊息能力。

更多關於即構ZIM介面與官方Demo可以點選參考這裡,對即構ZIM瞭解更多可以點選這裡

關於Avatar如何播報chatgpt內容,我們在下一篇文章實現。

5 相關程式碼

  1. nodejs接入chatgpt與即構zim