数字人视频合成开发指南

本文介绍数字人视频合成服务的接入流程和相关开发方案。

数字人视频合成服务(包括3D数字人视频合成和2D数字人视频合成)提供根据指定文本让数字人进行文本播报,平台会基于数字人播报的文本智能同步驱动数字人做出相应的嘴型、表情和动作,同时将渲染的数字人画面合成指定格式的视频文件。目前平台支持合成透明格式的视频和特定绿幕背景的非透明视频,更多信息可以参考下方的详细接入使用指南。

下面详细介绍下数字人视频合成服务的使用链路。

1. 完整的技术链路图

image

2. 核心链路介绍

2.1 提交数字人视频合成任务

  • 目的:

    • 提交数字人视频合成任务,获取到对应的任务ID。

  • 核心流程:

    • 通过调用虚拟数字人开放平台服务端SDK的提交视频合成任务API(目前平台服务端SDK支持java/python/php三种开发语言,具体接入方案可参考服务端API接入),获取到API返回的任务ID。

2.2 轮询数字人视频合成任务状态

  • 目的:

    • 根据上一步获取到的视频合成任务ID获取对应的任务状态。

  • 核心流程:

    • 通过调用虚拟数字人开放平台服务端SDK的查询视频合成任务状态API,获取对应视频合成任务的状态,由于数字人视频合成需要一定的时间,所以该接口需要定时轮询调用,建议轮询间隔3s,轮询过于频繁可能会导致查询失败。查询任务状态直到状态显示为已完成或者失败,状态为已完成的时候可以获取到对应的视频下载URL,然后直接通过URL可以下载到对应的视频,针对失败的任务可以根据对应的失败原因进行修改重新提交。

2.3 视频合成事件回调通知

  • 目的:

    • 通过接收视频合成事件回调,及时的了解视频合成任务是否完成。目前视频合成事件有:视频开始合成和视频合成结束。收到视频合成结束事件之后可以调用2.2的接口获取视频合成的结果信息。

  • 核心流程:

3. 数字人视频合成完整调用示例代码

3.1 引入二方包

<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>avatar20220130</artifactId>
  <version>${使用最新版本}</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>${选择一个合适版本}</version>
</dependency>

3.2 示例代码

package com.alibaba.avatar.sample;

import java.text.SimpleDateFormat;
import java.util.Date;

import com.aliyun.avatar20220130.Client;
import com.aliyun.avatar20220130.models.GetVideoTaskInfoRequest;
import com.aliyun.avatar20220130.models.GetVideoTaskInfoRequest.GetVideoTaskInfoRequestApp;
import com.aliyun.avatar20220130.models.GetVideoTaskInfoResponse;
import com.aliyun.avatar20220130.models.GetVideoTaskInfoResponseBody;
import com.aliyun.avatar20220130.models.SubmitAudioTo3DAvatarVideoTaskRequest;
import com.aliyun.avatar20220130.models.SubmitAudioTo3DAvatarVideoTaskRequest.SubmitAudioTo3DAvatarVideoTaskRequestApp;
import com.aliyun.avatar20220130.models.SubmitAudioTo3DAvatarVideoTaskRequest.SubmitAudioTo3DAvatarVideoTaskRequestAvatarInfo;
import com.aliyun.avatar20220130.models.SubmitAudioTo3DAvatarVideoTaskRequest.SubmitAudioTo3DAvatarVideoTaskRequestVideoInfo;
import com.aliyun.avatar20220130.models.SubmitAudioTo3DAvatarVideoTaskResponse;
import com.aliyun.avatar20220130.models.SubmitAudioTo3DAvatarVideoTaskResponseBody;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskRequest;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskRequest.SubmitTextTo3DAvatarVideoTaskRequestApp;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskRequest.SubmitTextTo3DAvatarVideoTaskRequestAudioInfo;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskRequest.SubmitTextTo3DAvatarVideoTaskRequestAvatarInfo;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskRequest.SubmitTextTo3DAvatarVideoTaskRequestVideoInfo;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskResponse;
import com.aliyun.avatar20220130.models.SubmitTextTo3DAvatarVideoTaskResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;

/**
 * <pre>
 *     虚拟数字人开放平台视频合成服务接入示例代码
 * </pre>
 *
 * @author avatar sample
 * @date 2023/07/11
 */
public class AvatarOfflineSample {
    private Client client;

    /**
     * 初始化
     *
     * @throws Exception
     */
    public void init() throws Exception {
        // 请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html
        String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
        String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
        client = createClient(accessKeyId, accessKeySecret);
    }

    /**
     * 使用AK&SK初始化账号Client
     *
     * @param accessKeyId
     * @param accessKeySecret
     * @return Client
     * @throws Exception
     */
    public Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
        Config config = new Config()
            // 必填,您的 AccessKey ID
            .setAccessKeyId(accessKeyId)
            // 必填,您的 AccessKey Secret
            .setAccessKeySecret(accessKeySecret);
        // 访问的域名
        config.endpoint = "avatar.cn-zhangjiakou.aliyuncs.com";
        return new Client(config);
    }

    /**
     * 提交一条文本合成任务,通过文本驱动数字人,合成对应的视频。详细参数介绍参考接入文档:https://help.aliyun.com/document_detail/447834.html
     *
     * @param tenantId
     * @param appId
     * @param avatarCode 数字人形象code
     * @param title      视频标题
     * @param text       视频文本内容
     */
    public String submitTextTask(Long tenantId, String appId, String avatarCode, String title, String text) throws Exception {
        // 设置appId信息
        SubmitTextTo3DAvatarVideoTaskRequestApp app = new SubmitTextTo3DAvatarVideoTaskRequestApp();
        app.setAppId(appId);

        // 设置合成的视频参数
        SubmitTextTo3DAvatarVideoTaskRequestVideoInfo videoInfo = new SubmitTextTo3DAvatarVideoTaskRequestVideoInfo();
            // 视频分辨率
        videoInfo.setResolution(4);
            // 是否包含字幕文件,当前字幕文件是单独提供一份字幕的ass文件
        videoInfo.setIsSubtitles(true);
            // 视频背景图
        videoInfo.setBackgroundImageUrl("https://nuwa-live-image.oss-cn-beijing.aliyuncs.com/yun-vh/image/video-system-bg/vcg_VCG211132322795_RF.png");
            // 是否透明视频,透明视频可用于二次编辑,
        videoInfo.setIsAlpha(false);

        // 设置合成的音频参数,包括TTS发音人、音速、音调、音量信息
        SubmitTextTo3DAvatarVideoTaskRequestAudioInfo audioInfo = new SubmitTextTo3DAvatarVideoTaskRequestAudioInfo();
            // TTS发音人,可以从平台获取,参考文档: https://help.aliyun.com/document_detail/479093.html
        audioInfo.setVoice("zhitian_emo");
            // 音频音量
        audioInfo.setVolume(50);
            // 音频语速
        audioInfo.setSpeechRate(0);
            // 音频音调
        audioInfo.setPitchRate(0);

        // 设置数字人形象参数
        SubmitTextTo3DAvatarVideoTaskRequestAvatarInfo avatarInfo = new SubmitTextTo3DAvatarVideoTaskRequestAvatarInfo();
            // 数字人形象code,3D形象需要调用3D接口,2D形象需要调用2D接口,本示例代码调用的是3D接口,所以这里需要使用3D形象code
        avatarInfo.setCode(avatarCode);
            // 数字人机位
        avatarInfo.setLocate(2);
            // 数字人角度
        avatarInfo.setAngle(0);
            // 数字人动作行业
        avatarInfo.setIndustryCode("live");

        SubmitTextTo3DAvatarVideoTaskRequest request = new SubmitTextTo3DAvatarVideoTaskRequest();
        request.setTenantId(tenantId);
        request.setApp(app);
        request.setTitle(title);
        request.setText(text);
        request.setVideoInfo(videoInfo);
        request.setAudioInfo(audioInfo);
        request.setAvatarInfo(avatarInfo);
        // 是否接收平台视频合成事件回调,具体可参考文档: https://help.aliyun.com/document_detail/2261152.html
        request.setCallback(false);

        RuntimeOptions runtime = new RuntimeOptions();
        SubmitTextTo3DAvatarVideoTaskResponse response = client.submitTextTo3DAvatarVideoTaskWithOptions(request, runtime);
        SubmitTextTo3DAvatarVideoTaskResponseBody responseBody = response.getBody();
        if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
            // 调用成功
            System.out.println(formatCurrentTime() + ":提交成功,taskUuid:" + responseBody.getData().getTaskUuid());
            return responseBody.getData().getTaskUuid();
        } else {
            // 调用失败
            System.out.println("提交失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("提交失败");
        }
    }

    /**
     * 提交一条音频合成任务,通过音频驱动数字人,合成对应的视频。详细参数介绍参考接入文档:https://help.aliyun.com/document_detail/447834.html
     *
     * @param tenantId
     * @param appId
     * @param avatarCode 数字人形象code
     * @param title      视频标题
     * @param audioUrl   音频url
     */
    public String submitAudioTask(Long tenantId, String appId, String avatarCode, String title, String audioUrl) throws Exception {
        // 设置appId信息
        SubmitAudioTo3DAvatarVideoTaskRequestApp app = new SubmitAudioTo3DAvatarVideoTaskRequestApp();
        app.setAppId(appId);

        // 设置合成的视频参数
        SubmitAudioTo3DAvatarVideoTaskRequestVideoInfo videoInfo = new SubmitAudioTo3DAvatarVideoTaskRequestVideoInfo();
        // 视频分辨率
        videoInfo.setResolution(4);
        // 视频背景图
        videoInfo.setBackgroundImageUrl("https://nuwa-live-image.oss-cn-beijing.aliyuncs.com/yun-vh/image/video-system-bg/vcg_VCG211132322795_RF.png");
        // 是否透明视频,透明视频可用于二次编辑,
        videoInfo.setIsAlpha(false);

        // 设置数字人形象参数
        SubmitAudioTo3DAvatarVideoTaskRequestAvatarInfo avatarInfo = new SubmitAudioTo3DAvatarVideoTaskRequestAvatarInfo();
        // 数字人形象code,D形象需要调用3D接口,2D形象需要调用2D接口,本示例代码调用的是3D接口,所以这里需要使用3D形象code
        avatarInfo.setCode(avatarCode);
        // 数字人机位
        avatarInfo.setLocate(2);
        // 数字人角度
        avatarInfo.setAngle(0);
        // 数字人动作行业
        avatarInfo.setIndustryCode("live");

        SubmitAudioTo3DAvatarVideoTaskRequest request = new SubmitAudioTo3DAvatarVideoTaskRequest();
        request.setTenantId(tenantId);
        request.setApp(app);
        request.setTitle(title);
        request.setUrl(audioUrl);
        request.setVideoInfo(videoInfo);
        request.setAvatarInfo(avatarInfo);
        // 是否接收平台视频合成事件回调,具体可参考文档: https://help.aliyun.com/document_detail/2261152.html
        request.setCallback(false);

        RuntimeOptions runtime = new RuntimeOptions();
        SubmitAudioTo3DAvatarVideoTaskResponse response = client.submitAudioTo3DAvatarVideoTaskWithOptions(request, runtime);
        SubmitAudioTo3DAvatarVideoTaskResponseBody responseBody = response.getBody();
        if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
            // 调用成功
            System.out.println(formatCurrentTime() + ":提交成功,taskUuid:" + responseBody.getData().getTaskUuid());
            return responseBody.getData().getTaskUuid();
        } else {
            // 调用失败
            System.out.println("提交失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("提交失败");
        }
    }

    /**
     * 查询任务状态
     *
     * @param tenantId
     * @param appId
     * @param taskUuid
     */
    public boolean queryTaskInfo(Long tenantId, String appId, String taskUuid) throws Exception {
        // 设置appId信息
        GetVideoTaskInfoRequestApp app = new GetVideoTaskInfoRequestApp();
        app.setAppId(appId);

        GetVideoTaskInfoRequest request = new GetVideoTaskInfoRequest();
        request.setTenantId(tenantId);
        request.setApp(app);
        request.setTaskUuid(taskUuid);

        RuntimeOptions runtime = new RuntimeOptions();
        GetVideoTaskInfoResponse response = client.getVideoTaskInfoWithOptions(request, runtime);
        GetVideoTaskInfoResponseBody responseBody = response.getBody();
        if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
            // 调用成功
            switch (responseBody.getData().getStatus()) {
                case "1":
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:等待执行...");
                    return false;
                case "2":
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:执行中...");
                    return false;
                case "3":
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:执行成功,视频URL:" + responseBody.getData().getTaskResult().getVideoUrl());
                    return true;
                case "4":
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:执行失败,失败原因:" + responseBody.getData().getTaskResult().getFailReason());
                    return true;
                case "5":
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:已取消");
                    return true;
                case "6":
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:已过期");
                    return true;
                default:
                    System.out.println(formatCurrentTime() + ":查询成功,当前任务状态:未知");
                    return true;
            }
        } else {
            // 调用失败
            System.out.println("查询失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("查询失败");
        }
    }

    public static void main(String[] args) throws Exception {
        // 开发者信息定义,获取方式参考文档:https://help.aliyun.com/document_detail/479093.html
        Long tenantId = null;
        String appId = null;
        // 数字人形象code,需要从开放平台获取,参考文档: https://help.aliyun.com/document_detail/479093.html
            // 本示例代码中使用到的一些动作code是根据daisy官方形象配置的,这里建议使用daisy官方形象code
        String avatarCode = null;

        int maxWait = 100;
        int waitInterval = 3000;

        AvatarOfflineSample sample = new AvatarOfflineSample();
        // 1. 初始化
        sample.init();

        // 2. 提交一个普通文本合成视频任务
            // 2.1 提交任务
        String title = "这是一个测试文本合成3D视频任务标题";
        String text = "当前时间是: " + formatCurrentTime() + ",这是一个测试文本合成3D视频任务内容,我是阿里云虚拟数字人官方数字人形象。";
        String taskUuid = sample.submitTextTask(tenantId, appId, avatarCode, title, text);

            // 2.2 轮询任务状态,建议通过接入平台视频合成事件回调监听合成完成状态,参考文档: https://help.aliyun.com/document_detail/2261152.html
        for (int i = 0; i < maxWait; i++) {
            boolean isOver = sample.queryTaskInfo(tenantId, appId, taskUuid);
            if (isOver) {
                break;
            }
            Thread.sleep(waitInterval);
        }

        // 3. 提交一个自定义数字人动作的文本合成视频任务(2D暂不支持自定义数字人动作)
        // 3.1 提交任务
        String title2 = "这是一个测试自定义数字人动作的文本合成3D视频任务标题";
        String text2 = "<speak>当前时间是<vh-action code=\"animation_4953\" interrupt=\"true\" />: " + formatCurrentTime() +
            ",这是一个测试自定义数字人动作的文本合成3D视频任务内容,大家好<vh-action code=\"animation_4960\" interrupt=\"true\" />,我是阿里云虚拟数字人官方数字人形象。</speak>";
        String taskUuid2 = sample.submitTextTask(tenantId, appId, avatarCode, title2, text2);

        // 3.2 轮询任务状态,建议通过接入平台视频合成事件回调监听合成完成状态,参考文档: https://help.aliyun.com/document_detail/2261152.html
        for (int i = 0; i < maxWait; i++) {
            boolean isOver = sample.queryTaskInfo(tenantId, appId, taskUuid);
            if (isOver) {
                break;
            }
            Thread.sleep(waitInterval);
        }

        // 4. 提交一个音频合成视频任务
        // 4.1 提交任务
        String title3 = "这是一个测试音频合成3D视频任务标题";
        String url = "https://virtualhuman.oss-cn-beijing.aliyuncs.com/platform/public/test_audio.mp3";
        String taskUuid3 = sample.submitAudioTask(tenantId, appId, avatarCode, title3, url);

        // 4.2 轮询任务状态,建议通过接入平台视频合成事件回调监听合成完成状态,参考文档: https://help.aliyun.com/document_detail/2261152.html
        for (int i = 0; i < maxWait; i++) {
            boolean isOver = sample.queryTaskInfo(tenantId, appId, taskUuid);
            if (isOver) {
                break;
            }
            Thread.sleep(waitInterval);
        }
    }

    private static String formatCurrentTime() {
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss,SSS");
        return format.format(new Date());
    }
}

以上就是一个完整的数字人视频合成服务的使用链路,关于数字人视频合成服务更多信息可参考下方详细的使用指南: