For the complete documentation index, see llms.txt. This page is also available as Markdown.

Convai 集成

Convai 集成 - 用于无缝集成的 PlayCanvas 插件指南。

在 URL 部分添加 Convai web-sdk-cdn 后,ConvaiClient 类将可直接在浏览器中使用。

将下面所有脚本添加到你的角色实体中。

Convai 初始化脚本

ConvaiNpc 脚本负责处理用户与由 Convai AI 驱动的虚拟角色之间的交互。

该脚本通过提供 API 密钥和角色 ID 来初始化 Convai 客户端。它设置了必要的回调,以处理各种事件,例如错误、用户查询以及来自 Convai 服务的音频响应。

initializeConvaiClient 函数是设置 Convai 客户端的入口点。它会创建 ConvaiClient 的新实例,并使用提供的 API 密钥、角色 ID 以及其他设置进行配置,例如启用音频和面部数据。

该脚本通过两种方式处理用户输入:通过表单进行文本输入,以及使用 "T" 键进行语音输入。对于语音输入, handleKeyDown 是位于 handleKeyUp 函数分别用于检测 "T" 键何时被按下和释放。当按下 "T" 键时,脚本会开始录制音频并将其发送到 Convai 服务进行处理。

ConvaiNpc.prototype.initialize 函数每个实体只会调用一次,并设置 Convai 客户端。它还注册了用于处理音频播放事件的回调,更新 isTalking 是位于 conversationActive 标志。

ConvaiNpc.prototype.handleAnimation 函数根据 isTalking 状态更新角色的动画,从而实现同步的口型动作和面部表情。

var ConvaiNpc = pc.createScript('convaiNpc');

/** 加载 convai-sdk cdn */
var convaiClient = null;
var userTextStream = ""; // 存储用户的文本输入
var npcTextStream = ""; // 存储 NPC 的文本回复
var keyPressed = false; // 记录 "T" 键是否被按下
var isTalking = false; // 指示 NPC 当前是否正在说话
var conversationActive = false; // 指示当前是否有正在进行的对话
var visemeData = []; // 存储用于口型同步动画的 viseme 数据
var visemeDataActive = false; // 指示是否有 viseme 数据可用
var formLoaded = false; // 指示表单是否已加载
let timeoutId;
/**
 * 使用 API 密钥、角色 ID 以及用于启用音频录制器和播放器的标志来初始化 ConvaiClient。请先从 Start() 调用此函数
 * 设置多个回调,以处理来自 Convai Client 的不同类型响应。
 * @param {string} apiKey - Convai 服务的 API 密钥。
 * @param {string} characterId - 在 Convai 服务中使用的角色 ID。
 * @param {boolean} enableAudioRecorder - 启用音频录制器的标志。
 * @param {boolean} enableAudioPlayer - 启用音频播放器的标志。
 * @param {number} faceModal - 角色要使用的 face modal。
 * @param {boolean} enableFacialData - 启用面部数据的标志。
*/
function initializeConvaiClient () {
    console.log("Convai client initiated.");
    convaiClient = new ConvaiClient({
        apiKey: "", // 替换为你的 API 密钥
        characterId: "", // 替换为你的角色 ID
        enableAudio: true,
        faceModal: 3,
        enableFacialData: true,
    });

    // 错误回调
    convaiClient.setErrorCallback(function(type, statusMessage) {
        console.log("Error Callback", type, statusMessage);
    });

    // 响应回调
    convaiClient.setResponseCallback(function(response) {
        // 处理用户查询
        if (response.hasUserQuery) {
            var transcript = response.getUserQuery();
            if (transcript) {
                var isFinal = transcript?.getIsFinal();
                var transcriptText = transcript.getTextData();
                if (isFinal) {
                    userTextStream += " " + transcriptText;
                }
                userTextStream = transcriptText;
            }
        }

        // 处理音频响应
        if (response.hasAudioResponse()) {
            var audioResponse = response.getAudioResponse();
            npcTextStream += " " + audioResponse.getTextData();

            // 处理用于口型同步动画的 viseme 数据
            if (audioResponse.hasVisemesData()) {
                let lipsyncData = audioResponse.getVisemesData().array[0];
                if (lipsyncData[0] !== -2) {
                    visemeData.push(lipsyncData);
                    visemeDataActive = true;
                }
            } else {
                visemeDataActive = false;
            }
        }
    });
}

// 处理按键按下事件以开始音频输入
function handleKeyDown(e) {
    const convaiFormEl = document.getElementById("convai-input");
    if (convaiClient && e.keyCode === 84 && !keyPressed && !(document.activeElement === convaiFormEl)) {
        e.stopPropagation();
        e.preventDefault();
        keyPressed = true;
        userTextStream = "";
        npcTextStream = "";
        convaiClient.startAudioChunk();
        conversationActive = true;
    }
}

// 处理按键抬起事件以停止音频输入
function handleKeyUp(e) {
    const convaiFormEl = document.getElementById("convai-input");
    if (convaiClient && e.keyCode === 84 && keyPressed && !(document.activeElement === convaiFormEl)) {
        e.preventDefault();
        keyPressed = false;
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
        convaiClient.endAudioChunk();
        }, 500);
    }
}

// 添加按键按下和按键抬起的事件监听器
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);

// 每个实体只调用一次的初始化代码
ConvaiNpc.prototype.initialize = function() {
    initializeConvaiClient();

    // 设置音频播放的回调
    convaiClient.onAudioPlay(() => {
        isTalking = true;
    });
    convaiClient.onAudioStop(() => {
        isTalking = false;
        conversationActive = false;
    });
};

// 根据说话状态处理动画
ConvaiNpc.prototype.handleAnimation = function() {
    this.entity.anim.setBoolean("Talking", isTalking);
};

// 每帧调用的更新代码
ConvaiNpc.prototype.update = function(dt) {
    this.handleAnimation();

    // 检查表单是否已加载并添加事件监听器
    if (!formLoaded && document.getElementById("convai-form")) {
        this.formListener();
        formLoaded = true;
    }
};

// 添加表单提交事件监听器
ConvaiNpc.prototype.formListener = function() {
    document.getElementById("convai-form").addEventListener("submit", (e) => {
        e.preventDefault();
        var inputValue = document.getElementById("convai-input").value;
        window.convaiClient.sendTextChunk(inputValue);
        userTextStream = inputValue;
        document.getElementById("convai-form").reset();
        conversationActive = true;
        npcTextStream = "";
    });
}

PlayerAnimationHandler 脚本

PlayerAnimationHandler 脚本负责根据某些条件(例如速度或其他因素)控制玩家角色的动画。

该脚本定义了三个属性:

  1. blendTime: 此属性控制动画之间的混合时间,决定动画过渡的平滑程度。默认值设为 0.2。

  2. velMin: 此属性表示触发特定动画所需的最小速度。默认值设为 10。

  3. velMax: 此属性表示触发特定动画所需的最大速度。默认值设为 50。

这些属性可以在编辑器中或通过代码进行调整,以微调玩家角色的动画行为。

initialize 函数在脚本初始化时调用。在此实现中,它使用指定的混合时间(this.blendTime)播放 'Idle' 动画。当玩家角色没有移动或速度超出 velMin 是位于 velMax.

该脚本旨在进一步扩展,以根据玩家角色的速度或其他条件处理不同的动画状态。例如,你可以添加额外的函数或逻辑来检查玩家的速度,并根据 velMin 是位于 velMax.

通过使用此脚本,你可以轻松管理玩家角色的不同动画之间的切换,进而为你的游戏或应用提供更具沉浸感和更真实的体验。

口型同步脚本

口型同步 脚本负责根据接收到的 viseme 数据来驱动角色的嘴部和面部表情动画。Viseme 是用于表示语音声音的关键口型和面部位置。该脚本会将 morph target 动画应用到角色的头部和牙齿组件上,以实现逼真的口型同步效果。

该脚本通过访问 visemeData 数组来工作,其中包含动画每一帧的 viseme 权重。然后,它将这些权重应用到头部和牙齿组件上对应的 morph target。 runVisemeData 函数通过遍历 viseme 权重并相应地设置 morph target 权重来处理此过程。

该脚本使用 currentVisemeFrame 变量和一个计时器变量来跟踪当前的 viseme 帧。这确保 viseme 动画与音频播放同步。当 viseme 数据播放完毕后, zeroMorphs 函数会被调用,将所有 morph target 权重重置为零,从而有效重置角色的面部表情。

头部追踪脚本

头部追踪 脚本负责根据摄像机的位置(代表用户视角)控制角色头部和眼睛的旋转。该脚本通过计算头部组件前向向量与摄像机前向向量之间的角度来实现这一点。如果该角度在指定阈值内(此处为 45 度),则头部和眼睛会旋转以朝向摄像机的位置。

将以上所有脚本添加到你的 playcanvas 项目中,并将 convaiNPC、lipsync、Headtracking 连接到 convi(你的模型)组件上。

附加脚本

最后更新于

这有帮助吗?