0

GPT虚拟直播Demo系列(二)|无人直播间实现虚拟人回复粉丝 - 程序员_Rya

 11 months ago
source link: https://www.cnblogs.com/RTCWang/p/17440359.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.

GPT虚拟直播Demo系列(二)|无人直播间实现虚拟人回复粉丝

虚拟人和数字人是人工智能技术在现实生活中的具体应用,它们可以为人们的生活和工作带来便利和创新。在直播间场景里,虚拟人和数字人可用于直播主播、智能客服、营销推广等。接入GPT的虚拟人像是加了超强buff,具备更强大的自然语言处理能力和智能对话能力,可以实现更加智能化、自然化的人机交互。

  • 直播主播:虚拟人可以作为直播间的主播角色,通过与粉丝的对话和互动,提高粉丝的互动效果和兴趣
  • 代替客服:数字人可以作为客服角色,通通过自然语言处理和智能对话,解决客户的问题,并提高客户满意度。
  • 营销推广:虚拟人可以作为品牌形象进行推广,数字人可以通过客观数据进行精准营销,提高粉丝的黏性和忠诚度。

续上一篇文章《「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间弹幕回复》 ,我们实现了ChatGPT与ZIM的对接。使得加入聊天群组就相当于加入了直播间,实时与ChatGPT文字互动。但还缺了点什么:直播间可不是只有文字,还有主播!接下来进入本文主题:如何接入虚拟人直播。

虚拟主播我们可以通过即构Avatar进行个人化定制,之前在他们《官网》体验过Avatar Demo,一键可以打造多元化风格,支持Q版、二次元、动漫、拟人等多种风格,即构自研虚拟形象引擎强大AI驱动能力,四种驱动方式:表情驱动、声音驱动、文本驱动、肢体驱动。根据本期Demo需求定制了拟人版本的主播小姐姐。即构AvatarQ版形象软萌可爱含丰富的服饰和妆容素材库,推荐大家去体验。即构Avatar的文本驱动方式刚好符合咱们的业务需求。

微信图片_20230529131522.png

1 加入ZIM房间,实时收发消息

加入ZIM房间跟上一篇文章介绍的nodejs版原理一致:

  1. 先登录ZIM
  2. 加入房间或创建房间
  3. 监听房间消息,如果来自ChatGPT则朗读

1.1 创建ZIM对象

首先引入ZIM库后,可以调用ZIM的create函数创建ZIM对象,然后调用ZIM对象的setEventHandler函数,将ZIMEventHandler对象传入。ZIMEventHandler主要用于处理一些回调事件如用户上线等回调事件。

public class ZIMMngr {
    /**
     * 创建ZIM对象
    */
    private ZIM createZIM(Application app, ZIMEventHandler handler) {
        // 创建 ZIM 对象,传入 APPID 与 Android 中的 Application
        ZIM zim = ZIM.create(KeyCenter.APP_ID, app);
        zim.setEventHandler(handler);
        return zim;
    }
    //其他代码略...
}

1.2 群聊-登录、创建房间、加入房间

登录即构服务首选需要token,生成token算法在附件源码已经给出,直接调用即可。但是需要注意,在这个Demo中直接在客户端上生成了,这是非常危险的操作,因为你的密钥和appid暴露出来了,黑客可以通过密钥和appid蹭你的额度费用。因此,建议把token计算放在服务器端生成。

ZIM的createRoom函数用于创建房间,需要提供房间号;joinRoom函数用于加入房间,同样也需要提供房间号。具体代码如下所示:

public class ZIMMngr {
    //其他代码略....
    /**
     * 登录zim
    */
    public void login(String userId, CB cb) {

        String token = ZIMMngr.getToken(userId);
        ZIMMngr.login(zim, token, userId, new ZIMLoggedInCallback() {
            @Override
            public void onLoggedIn(ZIMError errorInfo) {

                if (errorInfo.getCode() != ZIMErrorCode.SUCCESS) {
                    Log.e(TAG, "login error:" + errorInfo.getMessage());
                    cb.complete(false, "登录失败");
                } else {
                    cb.complete(true, null);
                }
            }
        });
    }
    /**
     * 加入房间
    */
    public void joinRoom(String roomId, CB cb) {
        zim.joinRoom(roomId, new ZIMRoomJoinedCallback() {
            @Override
            public void onRoomJoined(ZIMRoomFullInfo roomInfo, ZIMError errorInfo) {
                Log.e(TAG, ">>" + errorInfo.code);
                if (errorInfo.code == ZIMErrorCode.ROOM_DOES_NOT_EXIST) {
                    cb.complete(false, "房间不存在!");
                } else if (errorInfo.code == ZIMErrorCode.SUCCESS || errorInfo.code == ZIMErrorCode.THE_ROOM_ALREADY_EXISTS) {
                    cb.complete(true, roomInfo.baseInfo.roomName);
                }
            }
        });
    }
 
    /**
     * 创建房间
    */
    public void createRoom(String masterId, String roomId, String roomName, CB cb) {

        ZIMRoomInfo groupInfo = new ZIMRoomInfo();
        groupInfo.roomID = roomId;
        groupInfo.roomName = roomName;

        zim.createRoom(groupInfo, new ZIMRoomCreatedCallback() {
            @Override
            public void onRoomCreated(ZIMRoomFullInfo roomInfo, ZIMError errorInfo) {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {

                    inviteJoinRoom(masterId, roomId, CHATGPT_ID, cb);//这里把chagpt的用户id硬编码
                } else {
                    Log.e(TAG, "创建房间失败:" + errorInfo.message);
                    cb.complete(false, "房号已存在,请更换一个房间号!");
                }
            }
        });
    }
}

1.3 即时通讯实现收发消息

接下来实现消息收发,主动发送消息与监听接收消息。注意,这里因为我们只关注弹幕消息,因此非弹幕消息过滤。发送消息封装两类:

注意,因为我们这里只用弹幕消息,因此ROOM消息只表示弹幕消息。

public class ZIMMngr {
  
    //定义属性略....
    /**
     * 收到房间消息
    */
    private void onRcvMsg(ArrayList<ZIMMessage> messageList) {
        if (mListener == null) return;
        for (ZIMMessage zimMessage : messageList) {
            if (zimMessage instanceof ZIMBarrageMessage) {//只看弹幕消息
                ZIMBarrageMessage zimTextMessage = (ZIMBarrageMessage) zimMessage;
                if (zimMessage.getTimestamp() < this.startTime)
                    continue;
                String fromUID = zimTextMessage.getSenderUserID();
                ZIMConversationType ztype = zimTextMessage.getConversationType();
                String toUID = zimTextMessage.getConversationID();
                Msg.MsgType type = Msg.MsgType.P2P; 
                String data = zimTextMessage.message; 

                Msg msg = Msg.parseMsg(data, fromUID, toUID, ztype == ZIMConversationType.ROOM);
                mListener.onRcvMsg(msg);
            }
        }
    } 
        
    /**
     * 发送zim消息
     * */
    public void sendMsg(Msg msg, CB cb) {
        //p2p消息则发送Text,room发送弹幕类型消息
        ZIMMessage zimMsg = null;
        ZIMConversationType type;
        if (msg.type == Msg.MsgType.P2P) {
            ZIMTextMessage m = new ZIMTextMessage();
            m.message = msg.msg;
            zimMsg = m;
            type = ZIMConversationType.PEER;
        } else {
            ZIMBarrageMessage m = new ZIMBarrageMessage();
            m.message = msg.msg;
            zimMsg = m;
            type = ZIMConversationType.ROOM;
        }

        ZIMMessageSendConfig config = new ZIMMessageSendConfig();
        // 消息优先级,取值为 低:1 默认,中:2,高:3
        config.priority = ZIMMessagePriority.LOW;
        // 设置消息的离线推送配置
        ZIMPushConfig pushConfig = new ZIMPushConfig();
        pushConfig.title = "离线推送的标题";
        pushConfig.content = "离线推送的内容";
        config.pushConfig = pushConfig; 
        zim.sendMessage(zimMsg, msg.toUID, type, config, new ZIMMessageSentCallback() {
            @Override
            public void onMessageAttached(ZIMMessage message) {
            } 
            @Override
            public void onMessageSent(ZIMMessage message, ZIMError errorInfo) { 
                cb.complete(errorInfo.code == ZIMErrorCode.SUCCESS, errorInfo.message); 
            }
        }); 
    }  
    // 其他代码略....
}

上面代码只挑选了关键函数, 更多关于即构ZIM接口与官方Demo可以点击参考这里,或者参考附录源码。

2 创建虚拟形象-即构Avatar

接下来需要创建虚拟形象,读者可以参考官方文档获取更多详细信息。

需要注意的是,通过官方封装的ZegoCharacterHelper可以非常简单的创建Avatar。创建虚拟形象封装到setCharacter函数中,在程序初始化期间,需要执行initRes函数,将资源拷贝到SDCard。作为演示,这里是将Assets里面的相关资源拷贝到SDCard。在实际项目中,建议将资源存放在服务器端,通过离线下载的方式存储到SDCard。这样既可以降低安装包的大小,也更灵活。

public class AvatarMngr implements ZegoAvatarServiceDelegate {
 
    //属性定义略.... 
 
    /**
     * 设置虚拟形象如衣服、头发、性别等
    */
    private void setCharacter(User user) {  
        // 创建 helper 简化调用
        // base.bundle 是头模, human.bundle 是全身人模
        mCharacterHelper = new ZegoCharacterHelper(FileUtils.getPhonePath(mApp, "human.bundle", "assets"));
        mCharacterHelper.setExtendPackagePath(FileUtils.getPhonePath(mApp, "Packages", "assets"));
        // 设置形象配置
        mCharacterHelper.setDefaultAvatar(ZegoCharacterHelper.MODEL_ID_FEMALE);
        // 角色上屏, 必须在 UI 线程, 必须设置过avatar形象后才可调用(用 setDefaultAvatar 或者 setAvatarJson 都可以)
        mCharacterHelper.setCharacterView(user.avatarView, () -> {
        });
        mCharacterHelper.setViewport(ZegoAvatarViewState.half);
    
        mCharacterHelper.setPackage("ZEGO_Girl_Hair_0001");
        mCharacterHelper.setPackage("ZEGO_Girl_Tshirt_0001_0002");
        mCharacterHelper.setPackage("facepaint5");
        mCharacterHelper.setPackage("irises2");  
        updateUser(user);  
    }   
    private void initRes(Application app) {
        // 先把资源拷贝到SD卡,注意:线上使用时,需要做一下判断,避免多次拷贝。资源也可以做成从网络下载。
        if (!FileUtils.checkFile(app, "AIModel.bundle", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "AIModel.bundle", "assets");
        if (!FileUtils.checkFile(app, "base.bundle", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "base.bundle", "assets");
        if (!FileUtils.checkFile(app, "human.bundle", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "human.bundle", "assets");
        if (!FileUtils.checkFile(app, "Packages", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "Packages", "assets");

    }

    //...
    //其他代码略....
    //...
}

除了衣服、首饰、发型等"装饰类"形象定义,还可以捏脸,这里不详细描述,建议读者前往官网查看。即构Avatar官网。

4 直播间虚拟人与粉丝互动聊天

创建完虚拟人后,接下来将收到的ChatGPT消息朗读出来,使虚拟主播嘴巴动起来,互动玩法更好。首先执行initTextApi函数,初始化本地文字驱动引擎。接下来就可以调用ZegoTextAPI的playTextExpression函数,驱动虚拟人语音播报文字内容。

/**
 * 朗读文字(嘴唇+语音)
*/
public void playText(String text) {
    if (mTextApi == null) return;
    mTextApi.playTextExpression(text);
    Log.e(TAG, ">>>>已播放" + text);
}
/**
 * 初始化文本驱动接口
*/
private void initTextApi() {
    mTextApi = new ZegoTextAPI(mCharacterHelper.getCharacter());
    mTextApi.setTextExpressionCallback(new ITextExpressionCallback() {
        /**
         * 文本驱动播放启动时,回调
         */
        @Override
        public void onStart() {
            Log.d(TAG, "text drive start");
        }

        /**
         * 文本驱动播放出错时,回调
         * @param errorCode 错误码,详情请参考 [常见错误码 - 文本驱动](https://doc-zh.zego.im/article/14884#2)。
         */
        @Override
        public void onError(int errorCode, String msg) {
        }

        /**
         * 文本驱动播放结束时,回调
         */
        @Override
        public void onEnd() {
            Log.d(TAG, "text drive end");
        }
    });
}

文本驱动部分代码比较简单,也反映了官方对这块封装的比较好。播报文字主要借助即构avatar的文本能力,读者可以查看官方文档描述:官方文档。仔细阅读可以发现,关键核心代码非常少,附件里面的其他代码主要是开发App非核心代码。

本文演示了从0开发、无须服务端开发完成的基于ChatGPT的虚拟人直播,任何人下载该App即可加入直播间。一些小伙伴可能并不需要开发一个虚拟人直播平台,而是想着在抖音、快手、视频号等平台实现虚拟人直播。这里提供一个实现思路:

  1. 复用本文代码,可以实现ChatGPT回复、并将回复的文字驱动虚拟人
  2. 使用直播伴侣等工具录制虚拟人,推流到抖音平台
  3. 去github找开源工具,实时爬取直播间的弹幕
  4. 读取到弹幕后,调用ChatGPT得到回复,再回到第1步。

未来想象方面,虚拟人接入GPT可以实现更加智能化、个性化的服务,可以预见的是,未来的虚拟人将更加人性化,通过情感计算等技术,可以实现更加真实、自然的人机交互。虚拟人还可以与物理机器人结合,成为未来的机器人助手,为人们的生活和工作提供更加便利的服务。

5 Github源码

供一个实现思路:

  1. 复用本文代码,可以实现ChatGPT回复、并将回复的文字驱动虚拟人
  2. 使用直播伴侣等工具录制虚拟人,推流到抖音平台
  3. 去github找开源工具,实时爬取直播间的弹幕
  4. 读取到弹幕后,调用ChatGPT得到回复,再回到第1步。

未来想象方面,虚拟人接入GPT可以实现更加智能化、个性化的服务,可以预见的是,未来的虚拟人将更加人性化,通过情感计算等技术,可以实现更加真实、自然的人机交互。虚拟人还可以与物理机器人结合,成为未来的机器人助手,为人们的生活和工作提供更加便利的服务。

5 Github源码

  1. ChatGPT虚拟直播源码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK