如何实现智能体的形象动画

本文将为您详细介绍在AI实时互动Demo中,智能体形象动画的实现方式。

智能体形象动画

在语音通话场景中,您可以定制智能体形象,使其根据情绪标签实现情感切换。相比普通智能体,具备情绪输出能力的智能体能更自然地与用户互动,通过语气、表情和动作增强情感共鸣,提升交互的趣味性与真实感。它还能精准感知用户情绪,提供更贴合需求的服务,适用于客服、教育、娱乐等多种场景。

官方智能体形象动画实现介绍

实现介绍

AI实时互动官网Demo中,使用After Effects来设计智能体形象动画并导出为Lottie格式的资源文件,客户端集成Lottie组件库进行动画的播放与控制,Lottie详情,请参见LottieFiles

官方DemoAfter Effects中设计动画后,按照头部、手部、眼睛、捂眼睛进行模块拆分,并导出对应模块的动画资源,本方案使用到的所有的Lottie资源如下:

├── Avatar                       // 形象动画资源根目录
│   ├── bg.png                   // 背景图片
│   ├── Enter                    // 开场动画(通话接通中)
│   ├── Head                     // 头部动画  
│   ├── Hand                     // 手部动画
│   ├── CoveringEyes             // 手部捂眼睛动画(智能体讲话被打断效果)
│   ├── EyeEmotions              // 眼睛动画
│       ├── Interrupting         // 眼睛-被打断动画
│       ├── Listening            // 眼睛-聆听中动画
│       ├── Thinking             // 眼睛-思考中动画
│       ├── Speaking             // 眼睛-讲话中动画
│       ├── Happy                // 眼睛-讲话-开心动画
│       ├── Sad                  // 眼睛-讲话-伤心动画

实现参考

官网Demo的形象动画源码,参考如下:

平台

场景

文件名

类名

Android

开场动画

AUIAICallStartCallAnimator.java

AUIAICallStartCallAnimator

通话过程动画

AUIAICallOnCallingAnimator.java

AUIAICallOnCallingAnimator

iOS

开场动画

AUIAICallAgentAvatarAnimator.swift

AUIAICallStartCallAnimator

通话过程动画

AUIAICallOnCallingAnimator

Web

开场动画

HeroLottie.ts

HeroLottie

通话过程动画

实现说明

在官方Demo的展示中,智能体的头部(点头动作)与手部循环播放动画。当触发智能体的状态变化或情绪变化时,眼睛动画将循环执行;在智能体被打断时,手部捂眼睛的动画将单次执行,执行后返回至原有状态。此外,以下几个逻辑需特别处理:

  1. 头部上下循环动画时,眼睛也需要跟随上下移动,可以在Lottie中监听头部动画的播放进度来估算偏移的位置,并实时更新眼睛动画的位移。

    // 监听头部动画的更新事件
    headAnimator.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float animatedFraction = animation.getAnimatedFraction();
            float speed = headAnimator.getSpeed();
            float progress;
            if (speed < 0) {
                progress = 1 - animatedFraction;
            } else {
                progress = animatedFraction;
            }
            // 通过播放进度来估算偏移位移
            float y = eyeOffset * (progress - 1.0f);
            // 更新眼睛动画的位移
            eyeAnimatorContainerView.setTranslationY(y);
        }
    });
  2. 定义眼睛动画类型,并处理动画的切换逻辑:

    • 出错时最终状态,不能切换

    • 如果下一个是打断状态,不能打断

    • 当前打断状态,不能切换,需要缓存起来,等打断动画执行完成

    • 当前是带表情状态讲话时,不能切换为自然状态讲话

    • 下一个是打断状态或错误状态,需要等当前执行完成

    public enum EyeAnimator {
        None,// 非动画场景
        Start,// 智能体启动
        Error,// 智能体出错
        Interrupting,// 智能体被打断
        Thinking,// 智能体思考中,或者加载中
        Listening,// 智能体聆听中
        Speaking,// 智能体讲话中(自然形态)
        HappySpeaking,// 智能体讲话中(开心形态)
        SadSpeaking// 智能体讲话中(伤心形态)
    }
    
    public void setEyeAnimatorType(EyeAnimator type) {
        Log.d("Animator", "Will Set Animator Type Curr: " + currEyeAnimatorType + " To: " + type);
        if (type == EyeAnimator.None) {
            return;
        }
    
        if (currEyeAnimatorType == type) {
            return;
        }
    
        if (currEyeAnimatorType == EyeAnimator.Error) {
            // 出错时最终状态,不能切换
            return;
        }
    
        if (nextEyeAnimatorType == EyeAnimator.Interrupting || nextEyeAnimatorType == EyeAnimator.Error) {
            // 如果下一个是打断状态,不能打断
            return;
        }
        if (currEyeAnimatorType == EyeAnimator.Interrupting) {
            // 当前打断状态,不能打断,需要等继续执行完成
            if (type == EyeAnimator.Speaking && nextEyeAnimatorType.ordinal() > type.ordinal()) {
                // 下一个是带表情状态讲话时,下一个不能切换为自然状态讲话
                return;
            }
            nextEyeAnimatorType = type;
            return;
        }
    
        if (type == EyeAnimator.Speaking && currEyeAnimatorType.ordinal() > type.ordinal()) {
            // 当前是带表情状态讲话时,不能切换为自然状态讲话
            return;
        }
    
        if (type == EyeAnimator.Interrupting || type == EyeAnimator.Error) {
            // 下一个是打断状态或错误状态,需要等当前执行完成
            nextEyeAnimatorType = type;
            return;
        }
        Log.d("Animator", "Set Animator Type Next: " + type + " Curr: " + currEyeAnimatorType);
        startNextEyeAnimator(type);
    }
  3. 触发智能体打断事件时,捂眼睛动画需要与头部动画同步,在头部动画第一帧播放时同时开始执行(progres=0时),且在执行眼睛动画不能被切换到其他动画。

    // 监听头部动画播放结束事件
    headAnimator.addAnimatorListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            headAnimator.reverseAnimationSpeed();
            headAnimator.playAnimation();  // 头部动画需要循环播放,所以这里需要重新播放
            if (nextEyeAnimatorType != EyeAnimator.None) { // 记录下来的需要同步的动画类型
                startNextEyeAnimator(nextEyeAnimatorType); // 同步执行动画其他动画(例如:捂眼睛)
                if (currEyeAnimatorType == EyeAnimator.Interrupting) {
                    nextEyeAnimatorType = EyeAnimator.Listening;
                } else {
                    nextEyeAnimatorType = EyeAnimator.None;
                }
            }
        }
    });
  4. 由于形象动画比较难使用矢量方式进行设计,会使用较多切片,那么在加载Lottie时会占用较多的内存。为了减少内存消耗,在导出Lottie资源时可以仅导出单向动画,在播放时使用AutoReverse模式。

如何实现自定义智能体形象动画

方式一

按照官方Demo的方式,使用官方Demo的控制逻辑,您只需通过After Effects设计与导出自己的Lottie资源,然后进行替换官方的Lottie资源(资源路径:AUIAICall/src/main/assets/Avatar),完全复用官方Demo的动画播放与控制源码。

方式二

您自行在After Effects中设计动画,然后把需要根据状态变化的部位动画拆分出来(例如:聆听中、思考中、开心的讲话...),参考官方Demo中的动画播放控制逻辑进行控制实现,您也可以在控制实现中加入特殊逻辑的处理。