播报视频合成接入指南

本文详细介绍了通过API接入和使用播报视频合成服务的技术流程,包括模板准备、变量替换、视频合成及状态轮询等关键步骤。

操作流程

  1. 在视频创作工作台创建视频项目作为播报模板,并定义动态变量。

  2. 使用查询播报模板详情API获取模板信息和动态变量。

  3. 通过创建播报视频API动态设置变量值,提交视频合成。

  4. 通过查询视频API轮询视频合成状态,获取视频结果。

1 准备播报模板

使用API生成视频前,须前往视频创作控制台创建一个项目作为播报模板,具体操作教程见 数字人视频创作,并基于模板定义多个动态变量。创建各类变量的方式如下:

1.1 文本变量创建

在视频创作工作台左侧导航栏点击“内容”,选择文本输入,并通过{{}}作为占位符定义文本变量。

文本参数

1.2 图片变量创建

须先上传一个贴图素材作为占位,表达图片的大小和位置,选中贴图元素,点击菜单中“API”完成图片变量定义。

图片参数

2 获取播报模板信息和动态变量

  1. 通过列举播报模板API获取已创建的播报模板列表,从中获取模板ID。

  2. 使用查询播报模板详情API获取模板信息和待替换的变量。

示例响应如下:

{
  "id": "BS1b2WNnRMu4ouRzT4clY9Jhg",
  "name": "测试播报模板",
  "variables": [
    {
      "name": "slide_script",
      "type": "text"
    },
    {
      "name": "slide_image",
      "type": "image"
    }
  ]
}

模板ID也可以在视频创作工作台直接获取:

image

3 基于播报模板创建视频

说明

(可选)若希望替换贴图元素,需通过创建播报贴图API获取贴图ID,用于后续变量替换。针对图片变量,可通过设置fit参数控制图片填充方式,默认为contain。填充方式示例如下:

  • contain:保持图像宽高比,完整显示在容器内,可能留白但不裁剪。

  • cover:保持图像宽高比,完全填满容器,可能裁剪但不留白。

  • fill:拉伸图像以完全填满容器,不留白也不裁剪,但会失真变形。

选择播报模板,完成变量信息填充,通过创建播报视频API提交视频生成,获取对应的视频ID。

示例请求如下:

{
  "templateId": "BS1b2WNnRMu4ouRzT4clY9Jhg",
  "variables": [
    {
      "name": "slide_script",
      "type": "text",
      "properties": {
        "content": "待替换内容"
      }
    },
    {
      "name": "slide_image",
      "type": "image",
      "properties": {
        "resourceId": "M1smBGOteOqnwA7y0-YJgCoQ",
        "fit": "contain"
      }
    }
  ]
}

4 视频状态轮询

根据上一步获取到的视频ID通过查询视频API轮询视频合成状态。由于视频合成需要一定的时间,所以该接口需要定时轮询调用,建议轮询间隔3s,轮询过于频繁可能会导致查询失败。查询视频状态直到状态显示为成功或者失败,状态为成功的时候可以获取到对应的视频下载URL,然后直接通过URL下载到对应的视频,针对失败的视频可以根据对应的失败原因进行修改重新提交。

播报视频合成完整调用示例代码

引入SDK

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>lingmou20250527</artifactId>
    <!-- 请将 'the-latest-version' 替换为最新版本号:https://mvnrepository.com/artifact/com.aliyun/lingmou20250527 -->
    <version>${the-latest-version:1.4.0}</version>
</dependency>

示例代码

package com.alibaba.avatar.sample;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.lingmou20250527.Client;
import com.aliyun.lingmou20250527.models.*;
import com.aliyun.lingmou20250527.models.CreateBroadcastVideoFromTemplateRequest.CreateBroadcastVideoFromTemplateRequestVideoOptions;
import com.aliyun.lingmou20250527.models.GetUploadPolicyResponseBody.GetUploadPolicyResponseBodyData;
import com.aliyun.teaopenapi.models.Config;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;

public class BroadcastVideoDemo {
    private static final String ENDPOINT = "lingmou.cn-beijing.aliyuncs.com";
    private static final String STICKER_TYPE = "INPUT_BROADCAST_STICKER";

    public static void main(String[] args) throws Exception {
        // 0. 初始化sdk client
        Client client = BroadcastVideoDemo.getInstance();

        // 1. 上传贴图素材并创建贴图素材记录,如图片存在内容安全问题会失败(Optional,替换贴图元素时需要)
        File imageFile = new File("/path/to/sticker");
        GetUploadPolicyResponseBodyData uploadPolicy = upload(client, STICKER_TYPE, imageFile);
        String stickerId = createBroadcastSticker(client, uploadPolicy.getOssKey(), imageFile.getName());

        // 2.1 获取播报模板列表(获取 templateId)
        List<BroadcastTemplate> templates = listBroadcastTemplates(client);
        // 2.2 查询播报模板详情(返回可替换的元素 variables)
        BroadcastTemplate template = getBroadcastTemplate(client, templates.get(0).getId());

        // 3. 基于模板合成视频,失败情况
        // - 未开通商业化
        // - 播报场景中存在不合规元素(文本、音色、音频、图像等)
        String videoId = createVideoFromTemplate(client, template, stickerId);

        // 4. 视频状态轮询
        while (true) {
            BroadcastVideo video = getBroadcastVideo(client, videoId);
            String status = video.getStatus();
            if ("SUCCESS".equals(status)) {
                System.out.println("视频合成成功:" + JSON.toJSONString(video));
                break;
            } else if ("ERROR".equals(status)) {
                System.out.println("视频合成失败:" + JSON.toJSONString(video));
                break;
            } else {
                System.out.println("视频合成中...");
                Thread.sleep(3000);
            }
        }
    }

    private static String createBroadcastSticker(Client client, String ossKey, String fileName) throws Exception {
        CreateBroadcastStickerRequest request = new CreateBroadcastStickerRequest();
        request.setOssKey(ossKey);
        request.setFileName(fileName);
        return client.createBroadcastSticker(request).getBody().getData().getId();
    }

    private static List<BroadcastTemplate> listBroadcastTemplates(Client client) throws Exception {
        ListBroadcastTemplatesRequest request = new ListBroadcastTemplatesRequest();
        request.setPage(1);
        request.setSize(10);
        List<BroadcastTemplate> templates = client.listBroadcastTemplates(request).getBody().getData();
        System.out.println("播报模板列表:" + JSON.toJSONString(templates));
        return templates;
    }

    private static BroadcastTemplate getBroadcastTemplate(Client client, String templateId) throws Exception {
        GetBroadcastTemplateRequest request = new GetBroadcastTemplateRequest();
        request.setTemplateId(templateId);
        BroadcastTemplate template = client.getBroadcastTemplate(request).getBody().getData();
        System.out.println("播报模板详情:" + JSON.toJSONString(template));
        return template;
    }

    private static String createVideoFromTemplate(Client client, BroadcastTemplate template, String stickerId) throws Exception {
        CreateBroadcastVideoFromTemplateRequest request = new CreateBroadcastVideoFromTemplateRequest();
        // 设置播报模板id
        request.setTemplateId(template.getId());
        // 设置视频名称
        request.setName("播报视频合成API测试");
        // 设置视频选项
        // - fps: 帧率,支持[15, 30],默认:30
        // - resolution: 分辨率,支持["720p", "1080p"],默认:720p
        // - watermark: 是否添加水印,默认:true
        CreateBroadcastVideoFromTemplateRequestVideoOptions videoOptions =
            new CreateBroadcastVideoFromTemplateRequestVideoOptions();
        videoOptions.setFps(30);
        videoOptions.setResolution("720p");
        videoOptions.setWatermark(true);
        request.setVideoOptions(videoOptions);
        // 设置待替换的模板变量,支持替换图片、文本、数字人id、音色id
        TemplateVariable imageVar = new TemplateVariable();
        imageVar.setName("slide_image");
        imageVar.setType("image");
        imageVar.setProperties(new JSONObject()
            .fluentPut("resourceId", stickerId)
            .fluentPut("fit", "contain")
        );
        TemplateVariable textVar = new TemplateVariable();
        textVar.setName("slide_script");
        textVar.setType("text");
        textVar.setProperties(new JSONObject()
            .fluentPut("content", "待替换内容")
        );
        TemplateVariable avatarVar = new TemplateVariable();
        avatarVar.setType("avatar");
        avatarVar.setProperties(new JSONObject()
            .fluentPut("resourceId", "M1NLv93fW2a3uPT_Rc993QUA")
        );
        TemplateVariable voiceVar = new TemplateVariable();
        voiceVar.setType("voice");
        voiceVar.setProperties(new JSONObject()
            .fluentPut("resourceId", "M11E_ecrWkRozuIdlDnjzMAA")
        );
        request.setVariables(List.of(imageVar, textVar, avatarVar, voiceVar));

        BroadcastVideo video = client.createBroadcastVideoFromTemplate(request).getBody().getData();
        System.out.println("播报视频合成:" + JSON.toJSONString(video));
        return video.getId();
    }

    private static BroadcastVideo getBroadcastVideo(Client client, String videoId) throws Exception {
        ListBroadcastVideosByIdRequest request = new ListBroadcastVideosByIdRequest();
        request.setVideoIds(List.of(videoId));
        List<BroadcastVideo> videos = client.listBroadcastVideosById(request).getBody().getData();
        return videos.get(0);
    }

    private static GetUploadPolicyResponseBodyData upload(Client client, String type, File file) throws Exception {
        // 获取上传凭证
        GetUploadPolicyRequest request = new GetUploadPolicyRequest();
        request.setType(type);
        GetUploadPolicyResponseBodyData uploadPolicy = client.getUploadPolicy(request).getBody().getData();
        System.out.println("获取上传凭证:" + JSON.toJSONString(uploadPolicy));
        // HttpClient上传文件
        uploadFile(uploadPolicy, file);
        return uploadPolicy;
    }

    private static void uploadFile(GetUploadPolicyResponseBodyData data, File file) throws IOException {
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpPost uploadFile = new HttpPost("https://" + data.getOssPolicy().getHost());
            MultipartEntityBuilder builder = MultipartEntityBuilder.create();

            ContentType contentType = ContentType.create("multipart/form-data", Consts.UTF_8);

            // 添加OSS所需的表单字段
            builder.addTextBody("key", data.getOssKey() + "/" + file.getName(), contentType);
            builder.addTextBody("policy", data.getOssPolicy().getPolicy(), contentType);
            builder.addTextBody("OSSAccessKeyId", data.getOssPolicy().getAccessId(), contentType);
            builder.addTextBody("signature", data.getOssPolicy().getSignature(), contentType);
            // 添加文件
            builder.addBinaryBody("file", Files.newInputStream(file.toPath()), ContentType.IMAGE_PNG, file.getName());
            HttpEntity multipart = builder.build();
            uploadFile.setEntity(multipart);
            try (CloseableHttpResponse response = httpClient.execute(uploadFile)) {
                HttpEntity responseEntity = response.getEntity();
                System.out.println("Upload result: " + response.getStatusLine());
                if (responseEntity != null) {
                    String responseString = EntityUtils.toString(responseEntity);
                    System.out.println("Response content: " + responseString);
                }
                EntityUtils.consume(responseEntity);
            }
        }
    }

    private static Client getInstance() throws Exception {
        String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
        String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");

        Config config = new Config();
        // noinspection AklessInspection
        config.setAccessKeyId(accessKeyId);
        // noinspection AklessInspection
        config.setAccessKeySecret(accessKeySecret);
        config.setEndpoint(ENDPOINT);
        return new Client(config);
    }
}