“批量生产”、“快速裂变”和“去重”是制作营销短视频的关键,基于有限数量的基础素材大规模生成指定数量的新视频,是营销短视频创作的常见思路。本篇主要介绍一些经验方法,助您更快更高效地生产优质短视频。
概述
背景
进入5G时代,越来越多的商家选择短视频平台做营销推广,将广告制作成短视频投放在多个短视频营销号。
随着短视频内容的发展,视频生产的质量和效率已经越来越重要,本文从理论入手,介绍短视频生产中的方法和经验,本方案的示例基于智能媒体服务IMS,并附有示例代码,帮助大家更快上手短视频的批量合成。
目标读者
有短视频批量制作的商家,或视频批量生产工具的开发者。
方案介绍
先来看一个示例
生产此视频的素材是一段文字和一批视频,将文字转成人声朗读和字幕,再和视频进行合成
文字:
“人们懂得用五味杂陈形容人生,因为懂得味道是每个人心中固守的情怀。在这个时代,每一个人都经历了太多的苦痛和喜悦,人们总会将苦涩藏在心里,而把幸福变成食物,呈现在四季的餐桌之上”
视频:
这是一个非常常见的营销短视频,当策略敲定好后,通过替换素材、特效、文字,可批量生产出大量视频,从而达到批量生产的目的。
视频生产大概分“设计剧本-素材挑选-视频合成”几个步骤,我们来依次介绍其中的技巧:
1、分镜时长
视频中一个场景镜头我们称之为“分镜”,一个分镜长度不易超过3s,特别是在一个15s的短视频中,过长的分镜会让用户视觉疲劳,一般在电影中一个分镜也在2~3s左右。
如果剪辑中对分镜的时长没有特殊要求,可以对素材随机截取2s来使用,不足2s的取原素材时长。
2、素材卡点
上面的示例中,广告词转换后的人声,每句话对应一个视频素材,视频的开始结束时间正好卡在一句话的开始和结束,整个视频更有节奏感。
短视频虽然短,但一般也会有一个故事主线,可以是一段广告词或一段卡点音乐,一句话的朗读大约在2~3s,也正好贴合一个分镜的时长,人声-字幕-素材时间点完全对应,效果往往更好。
MPS智能生产接口提供了文字转语音、语音识别、音乐节奏检测能力,方便用户根据一个原始素材生产出故事主线。素材卡点的整体流程:
根据文字生成卡点:
根据卡点截取素材,并将截取后的视频素材放到时间线中
补充音乐、特效、转场,并合成成片
3、素材挑选
上面讲到了一个分镜的时长为2~3s,如果素材是由C端用户上传,或者运营采集的,建议加一个强限制,输入的素材时长不能短于2s,这样免去后续很多适配的麻烦。
实际上,2~3s的视频采集并不是难事,无论是拍摄美食、萌宠、还是街边,一个合理的运镜能采集到很好的一段短素材。
以下面这段素材为例,时长足够长,无论从第几秒开始截取,无论截取几秒,都是一个不违和的内容,不用担心对截取会对素材有影响。
4、准备模板
除了广告词和卡点音乐,还有一种剧本是来源于模板,客户会准备一些模板(如:美食类、探店类、萌宠类),合成时把素材填充进模板进行合成。
和前面提到的相同,合成时根据模板的要求,对素材进行截取。比如一个模板分别需要一个1s、2s、3s的素材,那就把素材分别进行截取,填充到模板中。
如果模板中需要一个3s的素材,但输入的最长素材是2s怎么办呢?
一般可以准备多套模板,分别适配多种时长的场景,当有素材输入时,根据素材时长挑选到合适的模板进行合成即可。
综上,一个好的生产流程是:
先对素材进行挑选,挑选出足够多的时长合适的素材,根据素材挑选合适的剧本,用剧本来对素材进行截取,然后合成。按照这个流程来制作,逻辑清晰、效果好、代码简洁。
相反的做法是: 用户上传的素材多种多样,业务方根据用户素材做大量复杂的适配,这样投入产出比低。无论是广告词,还是模板,对故事主线做了修改,效果都会大打折扣。
方案实施
下面介绍前面示例短视频的代码示例。
整体流程:
1、根据文字素材,调用MPS智能生产接口,生成人声音频和文字卡点
2、根据文字卡点截取视频素材,生成时间线,调用剪辑接口,合成成片
3、重复2,合成多条视频
接口说明
提供文字转语音、人声识别、节奏检测等能力
输入Timeline和成片地址,提交剪辑合成作业,返回剪辑作业JobId
示例代码
/**
Maven引入:
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ice20201109</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>mts20140618</artifactId>
<version>3.3.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
*/
package com.aliyun.ice.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.abs.common.util.MD5Util;
import com.aliyun.ice20201109.Client;
import com.aliyun.ice20201109.models.GetMediaProducingJobRequest;
import com.aliyun.ice20201109.models.GetMediaProducingJobResponse;
import com.aliyun.ice20201109.models.SubmitMediaProducingJobRequest;
import com.aliyun.ice20201109.models.SubmitMediaProducingJobResponse;
import com.aliyun.mts20140618.models.QueryIProductionJobRequest;
import com.aliyun.mts20140618.models.QueryIProductionJobResponse;
import com.aliyun.mts20140618.models.SubmitIProductionJobRequest;
import com.aliyun.mts20140618.models.SubmitIProductionJobResponse;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.teaopenapi.models.Config;
import com.google.common.io.CharStreams;
import org.apache.commons.io.Charsets;
import org.apache.logging.log4j.util.Strings;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Create by oushu
* Date 2021/12/7 上午9:28
*/
public class BatchProduceTTSSubtitleRelease {
private String accessKeyId;
private String accessKeySecret;
private OSS ossClient;
private String bucket;
private String regionId;
private com.aliyun.mts20140618.Client mpsClient;
public void initClient() throws Exception {
accessKeyId = "your_ak";
accessKeySecret = "your_sk";
bucket = "your-bucket";
regionId = "cn-shanghai";
ossClient = createOssClient();
mpsClient = createMpsClient();
}
public static void main(String[] args) throws Exception {
BatchProduceTTSSubtitleRelease batchProduceVideo = new BatchProduceTTSSubtitleRelease();
batchProduceVideo.initClient();
batchProduceVideo.batchProduceVideo();
}
public void batchProduceVideo() throws Exception {
// 文字素材
String text = "人们懂得用五味杂陈形容人生,因为懂得味道是每个人心中固守的情怀。在这个时代,每一个人都经历了太多的苦痛和喜悦,人们总会将苦涩藏在心里,而把幸福变成食物,呈现在四季的餐桌之上";
// 视频素材
List<String> videoUrlList = new ArrayList();
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_1.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_2.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_3.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_4.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_5.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_6.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_7.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_8.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_9.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_10.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_11.mp4");
videoUrlList.add("https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your_video_12.mp4");
// 背景音乐
String bgMusic = "https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/iproduction/demo/music/generated_6_good.wav";
// 字幕样式设置
Integer fontSize = 45;
String fontName = "WenQuanYi Zen Hei Mono";
String fontColor = "#FFFFFF";
// 视频尺寸
Integer width = 720;
Integer height = 1280;
// logo
String logoUrl = "https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/your-logo.png";
Integer logoX = 20;
Integer logoY = 20;
String title = "这里是标题";
String subTitle = "这里是副标题";
// 成片数量
int targetCount = 2;
// 每次提交一个任务,业务方根据需要更换不同参数提交多次
produceSingleVideo(text, videoUrlList, title, subTitle, bgMusic, fontSize, fontName, fontColor, logoUrl, logoX, logoY, width, height, targetCount);
}
// 提交单个任务
public void produceSingleVideo(String text, List<String> videoUrls, String title, String subtitle, String bgMusic, int fontSize, String fontName, String fontColor,
String logoUrl, Integer logoX, Integer logoY, int width, int height, int targetCount) throws Exception {
// 提交MPS任务,文字生成语音和字幕
String voice = "zhichu";
String jobParams = "{\"voice\":\"" + voice + "\",\"format\":\"mp3\",\"sample_rate\":16000}";
String textObject = "iproduction/20211214/" + MD5Util.getMd5String(text) + ".txt";
text = text.replaceAll(",", "。"); // AsyncTextToSpeech任务用句号进行断句
putObjectContent(textObject, text);
String input = "oss://" + bucket + ".oss-" + regionId + ".aliyuncs.com/" + textObject;
System.out.println("input : " + input);
SubmitIProductionJobRequest submitIProductionJobRequest = new SubmitIProductionJobRequest();
submitIProductionJobRequest.setFunctionName("AsyncTextToSpeech");
submitIProductionJobRequest.setInput(input);
submitIProductionJobRequest.setOutput("oss://" + bucket + ".oss-" + regionId + ".aliyuncs.com/iproduction/20220617/{source}-{sequenceId}.{resultType}");
submitIProductionJobRequest.setJobParams(jobParams);
SubmitIProductionJobResponse submitIProductionJobResponse = mpsClient.submitIProductionJob(submitIProductionJobRequest);
System.out.println("submit mps response : " + JSONObject.toJSONString(submitIProductionJobResponse.body));
// 等待任务完成
String jobId = submitIProductionJobResponse.body.jobId;
String result;
while (true) {
QueryIProductionJobRequest queryIProductionJobRequest = new QueryIProductionJobRequest();
queryIProductionJobRequest.setJobId(jobId);
QueryIProductionJobResponse queryIProductionJobResponse = mpsClient.queryIProductionJob(queryIProductionJobRequest);
System.out.println("job info : " + JSONObject.toJSONString(queryIProductionJobResponse.body));
if ("Success".equals(queryIProductionJobResponse.body.state)) {
result = queryIProductionJobResponse.body.result;
break;
}
Thread.sleep(5000);
}
// 获取生成的音频和字幕
JSONObject resultObj = JSONObject.parseObject(result);
String dataString = resultObj.getString("Data");
System.out.println("data : " + dataString);
String audioObject = null;
String subtitleObject = null;
JSONObject dataObject = JSONObject.parseObject(dataString);
JSONArray array = dataObject.getJSONArray("result");
for (Object obj : array) {
JSONObject jsonObject = (JSONObject)obj;
String object = jsonObject.getString("file");
if (object.endsWith("mp3")) {
audioObject = object;
} else if (object.endsWith("txt")) {
subtitleObject = object;
}
}
System.out.println("audioObject : " + audioObject);
System.out.println("subtitleObject : " + subtitleObject);
// 获取字幕内容
String subtitleContent = getObjectContent(subtitleObject);
System.out.println("subtitleContent : " + subtitleContent);
// 组装字幕轨
if (fontSize <= 0) {
fontSize = 32;
}
if (Strings.isBlank(fontName)) {
fontName = "WenQuanYi Zen Hei Mono";
}
if (Strings.isBlank(fontColor)) {
fontColor = "#000000";
}
for (int j = 0; j < targetCount; j++) {
// 每次循环将视频素材随机,并提交合成任务
Collections.shuffle(videoUrls);
JSONArray subtitleTrackClips = new JSONArray();
JSONArray mpsSubtitles = JSONArray.parseArray(subtitleContent);
JSONArray videoTrackClips = new JSONArray();
// 字幕距离底部距离
float subtitleBottom = 0.25f;
// 随机特效,更多特效见:https://help.aliyun.com/document_detail/207059.html
List<String> vfxs = Arrays.asList("heartfireworks", "colorfulradial", "meteorshower", "starry", "colorfulstarry", "moons_and_stars", "flyfire", "starexplosion", "spotfall", "sparklestarfield");
// 随机转场,更多转场见:https://help.aliyun.com/document_detail/204853.html
List<String> transitions = Arrays.asList("windowslice", "displacement", "bowTieVertical", "linearblur", "waterdrop", "polka", "wiperight", "gridflip", "hexagonalize", "windowblinds", "风车");
float transDuration = 0.3f;
Collections.shuffle(vfxs);
for (int i = 0; i < mpsSubtitles.size(); i++) {
JSONObject mpsSubtitle = mpsSubtitles.getJSONObject(i);
String content = mpsSubtitle.getString("text");
content = content.replaceAll("。", "");
Float timelineIn = mpsSubtitle.getFloat("begin_time") / 1000;
Float timelineOut = mpsSubtitle.getFloat("end_time") / 1000;
String subtitleClip = "{\"Content\":\"" + content + "\",\"TimelineIn\":" + timelineIn + ",\"TimelineOut\":" + timelineOut +
",\"Type\":\"Text\",\"X\":0.0,\"Y\":" + subtitleBottom + ",\"Font\":\"" + fontName + "\",\"Alignment\":\"BottomCenter\",\"FontSize\":" + fontSize +
",\"FontColor\":\"" + fontColor + "\",\"OutlineColour\":\"#000000\",\"FontColor\":\"#ffffff\",}";
subtitleTrackClips.add(JSONObject.parseObject(subtitleClip));
// 根据字幕轨,截取视频轨片段
float out = timelineOut - timelineIn;
// 随机特效
String vfx = vfxs.get(i % vfxs.size());
String transition = transitions.get(i % transitions.size());
String url = videoUrls.get(i % videoUrls.size());
JSONObject clip = new JSONObject();
clip.put("MediaURL", url);
if (url.endsWith(".jpg")) {
clip.put("Duration", out + transDuration);
clip.put("Type", "Image");
} else {
clip.put("Out", out + transDuration);
}
JSONArray effects = new JSONArray();
// 添加背景模糊
effects.add(JSONObject.parseObject("{\"Type\":\"Background\",\"SubType\":\"Blur\",\"Radius\":0.1}"));
// 添加氛围类特效
effects.add(JSONObject.parseObject("{\"Type\":\"VFX\",\"SubType\":\"" + vfx + "\"}"));
// 视频静音
effects.add(JSONObject.parseObject("{\"Type\":\"Volume\",\"Gain\":0}"));
// 添加转场
effects.add(JSONObject.parseObject("{\"Type\":\"Transition\",\"SubType\":\"" + transition + "\",\"Duration\":"+transDuration+"}"));
clip.put("Effects", effects);
videoTrackClips.add(clip);
}
if (title != null && title.length() > 0) {
float titleY = 280;
int titleFontSize = 70;
String titleFont = "AlibabaPuHuiTi";
String titleClip = "{\"Type\":\"Text\",\"X\":0,\"Y\":" + titleY + ",\"Font\":\"" + titleFont + "\",\"Content\":\"" + title + "\",\"Alignment\":\"TopCenter\",\"FontSize\":" + titleFontSize + ",\"FontColor\":\"#FFD700\",\"Outline\":4,\"OutlineColour\":\"#000000\",\"FontFace\":{\"Bold\":true,\"Italic\":false,\"Underline\":false}}";
subtitleTrackClips.add(JSONObject.parse(titleClip));
}
if (subtitle != null && subtitle.length() > 0) {
float subtitleY = 200;
int subtitleFontSize = 60;
String subtitleFont = "AlibabaPuHuiTi";
String subtitleClip = "{\"Type\":\"Text\",\"X\":0,\"Y\":" + subtitleY + ",\"Font\":\"" + subtitleFont + "\",\"Content\":\"" + subtitle + "\",\"Alignment\":\"TopCenter\",\"FontSize\":" + subtitleFontSize + ",\"FontColorOpacity\":1,\"FontColor\":\"#ffffff\",\"Outline\":2,\"OutlineColour\":\"#000000\",\"FontFace\":{\"Bold\":false,\"Italic\":false,\"Underline\":false}}";
subtitleTrackClips.add(JSONObject.parse(subtitleClip));
}
// 组装音频轨
String audioUrl = "http://" + bucket + ".oss-" + regionId + ".aliyuncs.com/" + audioObject;
String audioTrackClips = "";
if (Strings.isBlank(bgMusic)) {
audioTrackClips = "[{\"MediaURL\":\"" + audioUrl + "\"}]";
} else {
// 两个音频轨,一个人声,一个音乐
audioTrackClips = "[{\"MediaURL\":\"" + audioUrl + "\"}]},{\"AudioTrackClips\":[{\"MediaURL\":\"" + bgMusic + "\"}]";
}
// 图片轨,用于展示logo
String logoClip = "";
int logoWidth = 200;
int logoHeight = 60;
if (Strings.isNotBlank(logoUrl)) {
logoClip = "{\"ImageURL\":\"" + logoUrl + "\",\"X\":" + logoX + ",\"Y\":"
+ logoY + ",\"Width\":\"" + logoWidth + "\",\"Height\":\"" + logoHeight + "\"}";
}
// 拼时间线
String timeline = "{\"VideoTracks\":[{\"VideoTrackClips\":" + videoTrackClips.toJSONString() + "}]," +
"\"SubtitleTracks\":[{\"SubtitleTrackClips\":" + subtitleTrackClips.toJSONString() + "}]," +
"\"AudioTracks\":[{\"AudioTrackClips\":" + audioTrackClips + "}]," +
"\"ImageTracks\":[{\"ImageTrackClips\":[" + logoClip + "]}]}";
System.out.println("timeline : " + timeline);
// 提交合成任务
SubmitMediaProducingJobRequest submitMediaProducingJobRequest = new SubmitMediaProducingJobRequest();
submitMediaProducingJobRequest.setTimeline(timeline);
String outputPath = IceUtil.getRandomOutputPath();
String mediaURL = "https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/" + outputPath + ".mp4";
submitMediaProducingJobRequest.setOutputMediaConfig("{\"MediaURL\":\"" + mediaURL + "\",\"Width\":" + width + ",\"Height\":" + height + "}");
Client iceClient = TestClientInstance.getInstance().getIceClient();
SubmitMediaProducingJobResponse submitMediaProducingJobResponse = iceClient.submitMediaProducingJob(submitMediaProducingJobRequest);
System.out.println("job created, jobId : " + submitMediaProducingJobResponse.body.jobId + ", requestId : " + submitMediaProducingJobResponse.body.getRequestId() + ", mediaURL : " + mediaURL);
// 等待合成任务完成
while (true) {
GetMediaProducingJobRequest getMediaProducingJobRequest = new GetMediaProducingJobRequest();
getMediaProducingJobRequest.setJobId(submitMediaProducingJobResponse.body.jobId);
GetMediaProducingJobResponse getMediaProducingJobResponse = iceClient.getMediaProducingJob(getMediaProducingJobRequest);
System.out.println("GetMediaProducingJobResponse : " + JSONObject.toJSONString(getMediaProducingJobResponse.body));
String status = getMediaProducingJobResponse.getBody().getMediaProducingJob().getStatus();
if ("Success".equals(status)) {
break;
}
Thread.sleep(5000);
}
System.out.println("Produce succeed : " + mediaURL);
}
}
public com.aliyun.mts20140618.Client createMpsClient() throws Exception {
String accessKeyId = TestClientInstance.getInstance().getAk();
String accessKeySecret = TestClientInstance.getInstance().getSk();
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = "mts." + regionId + ".aliyuncs.com";
return new com.aliyun.mts20140618.Client(config);
}
public OSS createOssClient() {
String endpoint = "http://oss-" + regionId + ".aliyuncs.com";
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
return ossClient;
}
public String getObjectContent(String object) throws Exception {
OSSObject obj = ossClient.getObject(bucket, object);
InputStream stream = obj.getObjectContent();
String result = CharStreams.toString(new InputStreamReader(stream, Charsets.UTF_8));
return result;
}
public com.aliyun.ice20201109.Client createIceClient() throws Exception {
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = "ice." + regionId + ".aliyuncs.com";
return new com.aliyun.ice20201109.Client(config);
}
public void putObjectContent(String object, String content) throws Exception {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, object, new ByteArrayInputStream(content.getBytes()));
ossClient.putObject(putObjectRequest);
}
}