作者 | 百度人工智能创作团队
导读
本文从业务出发,系统介绍了采编式 TTV的实现逻辑和实现路径。结合业务拆解,实现了一个轻量级服务编排引擎,有效实现业务诉求、高效支持业务扩展。
全文6451字,预计阅读时间17分钟。
近年来,内容视频化趋势仍在持续,短视频的市场规模持续增长,2022年8月CNNIC发布的数据显示,截至2022年6月,我国网民规模为10.51亿,占网民整体的91.5%。随着大量短视频内容充斥网络,提高视频生产效率和效果的半智能化、辅助创作工具如视频剪辑、视频美化等如雨后春笋般涌现,视频生产形态不断升级。百家号作为百度为内容创作者打造的内容生产平台,在内容生产方面深耕多年,如能利用百度强大的 AI 能力,以当前百家号图文内容为脚本,实现视频智能化自动、半自动生产,将会进一步降低视频创作者的创作成本,带来视频创作的进一步发展。
自 AIGC 项目启动之后,我们对视频自动生产方案进行了一系列摸索试验,最终沉淀出一套完整的解决方案——采编式视频自动生产。该方案基于一系列微服务的配合执行,如何高效、稳定地完成整个流程的组织与调度是其中一个重要的课题。另外,在早期,整个项目的迭代非常迅速,业务发展变化比较大,如何较好地支持系统扩展与升级,也是我们关注的重点。本文将系统介绍采编式 AIGC 视频生产流程的实现方案。
所谓的采编式视频生产,顾名思义,即基于图文,进行相关视频和图片素材的补充和添加。由图文到视频的过程,看似简单,但作为完全不同的两种内容形态,这其中还有许多工作要做,按照一般处理方法,主要有以下内容:
文本处理:由于整个视频是用图文做脚本来完成的,所以,视频主体抽取(这个视频讲述的是什么内容)、视频调性确认(阳春白雪还是下里巴人)、视频字幕/旁白生成等,都需要基于充分的内容理解,再进行精准的文章主体识别、文章风格识别、口播逐字稿改写、字幕拆分等工作的进行;
素材处理:采编式视频生产的核心,是要将碎片化的素材基于图文脚本进行合理的编排,故而进行视频和图片素材的在线检索、剪裁、清洗等必不可少;
语音处理:语音播报作为视频的关键元素,在视频生产中是必不可少的一环,需要基于图文进行合理的语音合成与添加;
其他视频元素添加:视频标注、水印、动效、背景音乐、背景视频、前置氛围渲染等元素的添加,能够更好地丰富视频效果;
视频合成:将采编好的脚本文件,利用视频合成技术进行视频渲染输出。
△图1 采编式视频生产
如图1,不同于一般的业务流程,采编式视频生产需要基于大量的媒体数据处理,整个处理过程是无人工干预的全自动化过程,如何将这些服务进行有效地编排与调度,是整个视频生产的关键问题。
常见的服务编排,一般都采取利用定时任务、消息队列、持久化存储等工具进行微服务的拼接串联。这个方案需要在流程中定义关键的状态节点,来标记每个微服务的执行状态,并将状态记录到 MySQL 等 持久化存储中,再通过定时任务或者消息队列来驱动整个流程的流转。
△图2 状态机流程调度
可以看到,该方案是一个可控性较高的流程编排与调度的方案,整个系统的复杂度、稳定性与业务复杂度、系统设计合理性息息相关,更适合一些变动较大、相对轻量级的业务。
随着互联网技术的不断发展、微服务的普及,服务编排的解决方案也日益成熟,涌现出一批成熟优秀的服务编排引擎。业内比较成熟的服务编排引擎有 Cadence、Temporal、Conductor等。
服务编排引擎会进行基础的流程、任务、节点等基础元素的定义,提供流程启动、任务调度、状态监控等基础能力,具备对于编排完成的服务或者流程在运行时进行动态、端到端可视化监控的能力。以 Cadence 编程模型为例说明一般编排引擎的编程模型:
△图3. Cadence编程模型
服务编排引擎一般都有一个中央调度系统,同时提供一些外部可调 api,开发人员只需要通过对框架能力的调用来实现业务逻辑而不需要关注系统的调度运行,甚至包括系统的超时处理、失败重试、异常兜底,框架都会代为处理,提升业务研发的效率。相应地,成熟的框架都有一定的接入门槛和运维成本,比较适合大型项目。
由于 AIGC 视频生产业务发展迅速,迭代速度非常快,对成熟流程调度框架的调研中,遇到了系统利用率低、问题追查成本高的问题,为了快速支持业务、保障系统的稳定性与可用性,我们谨慎地选择了基于状态的流程调度方案,并在此基础上参考流程编排框架的思想,建设一套底层中央编排器,驱动上层微服务的执行。整体思想可以概括为:
从上而下地,基于功能对整个流程进行模块拆分、基于实现对模块进行组件拆分,对模块进行状态管理、对组件进行位值管理
利用消息队列实现流程串联,通过对状态与位值的判断实现流程调度
通过对模块与组件的组合配置实现流程组织
首先基于对需求的理解,对整个编排流程进行了模块拆分,并对每一个模块进行相关的状态赋值,拆分的模块有:
图文接入模块:接入上层业务或者外部业务的文本内容输入,进行基础的数据解析、校验、打平与过滤功能;
脚本编排模块:实现从图文到视频脚本的生成功能,该模块的输入为图文,输出为编排好的视频脚本,包含三条时间轴:①素材轴②文本与语音轴③挂件轴,定义了视频任意一个时间点对应的文本、素材与相关挂件。视频脚本不仅可以用于视频渲染,还可输出给用户作为视频编辑的草稿;
视频合成模块:实现从视频脚本到视频文件的生成,该模块执行完成之后就已经产生了可播放的视频文件,标志着视频生成完成;
视频输出模块:将视频文件按照业务需求输出,包括但不限于发布到百家号、回传业务方等。
在整个生产流程中,完成一个视频的生产,所需要的功能模块是固定的,但是实现的方式与方法可能会持续地扩展与迭代,为了便于后续状态的管理与功能的扩展,采取了大的功能模块包含小的功能组件的方式,这种方式的优点有二:
方便数据输出:在脚本编排模块完成之后进行视频脚本的输出并提供给多个业务方使用,无论模块内部如何扩展,脚本输出的时机是固定的,视频文件的输出同理;
方便功能扩展:随着业务的发展,功能实现的方案升级甚至替换是不可避免的,模块内部提供原子化功能组件,可以方便地进行单功能的升级迭代或者添加,而不影响整体其他组件
为了方便微服务的调用状态管理,我们又为每个微服务赋予了位值,所谓位值是当前组件在一个64位整数所处的二进制位次,每个组件占据两位,枚举标记成功和失败状态,我们只需要校验对应位次的值,即可判断当前组件的调用状态与返回状态。
△图4 模块与组件拆分
至此,我们通过『状态』实现了对整个生产流程模块的管理,又通过『调用位』、『返回位』实现了对具体组件的管理。其中,状态管理较好理解,主要是通过持久化存储一个状态字段,来标记当前流程所处模块,如图3所示,当某一条视频生成任务状态值为INIT时即可知当前任务处于视频脚本编排模块,但是具体在执行哪个或者哪些微服务呢?如上文所言是通过位值来确认的,对于位值的应用相对较为复杂,下面我们就详细阐述一下位值的应用。
△图5槽位值原理示意图
如图5所示,『调用位』、『返回位』都是一个 UINT64整数,每两位组合可以有4个状态,我们取前三个状态进行调用或返回状态的表示。每一个组件在注册进入系统时,都会先分配一个位次(如图3所示,1即表示占据槽位值的低两位),如此一来,某个组件状态发生变更时通过二进制操作修改对应二进制位的值即可。
该方案的优点是能够通过一个整形值管理32个组件的请求或返回状态,且每个组件的状态修改互不影响。当然这也带来一个问题,即该方案最多只能管理32个组件,更多组件需要管理时就要扩展字段或者采取其他方案,同时虽然变更某个组件槽位值不影响其他组件,但当出现服务并行需要将修改后的槽位值更新存储时,需要确保更新的事务性,这个问题的解决我们会在后面的流程调度中完成。
在完成了组件与模块的拆分与确定之后,即可根据业务逻辑,基于组件之间的相互依赖关系进行流程编排配置。流程搭建采取配置化、插拔式方案,将业务所需组件放进对应模块,编排出所需的视频生产流程,如图5所示为当前采编式 AIGC 视频生产流程的流程图,在当前业务状态下,存在相互依赖关系的组件如图文理解、插件选择、文本处理在整个流程中串行执行,有相同前置依赖但彼此不依赖的组件如素材生成、素材检索、语音合成则应该并行执行:
△图6 采编式 AIGC 视频生产流程
如要实现一个任务流程,按照上述流程图执行,那么首先需要有这样一个流程描述文件,该文件按照一定的规则组织,包含一个流程完成所需的所有组件,并能够准确描述这些组件的执行顺序与相互依赖关系,在此基础上,如能描述当前组件所处模块、状态,那么对于流程理解以及后续流程执行都有很大助益。基于以上考虑,我们采取以组件为最小单位,组合生成配置文件:
{
……
{ // 脚本编排模块
"module_name":"ScriptAssign",
"status":"init",
"next_status":"generating",
"components":[
……
{
"component_name" : "TextProcessor", // 组件名称,文本处理组件
"slot_index":2, // 组件位次,第三位(index从0开始),表示低第五六两个二进制位
"slot_num_success": 16, // 2^(2*slot_index) 成功时,要将『低第五位』置为1,同时确保『低第六位』为0,具体在进行位置计算时实现
"slot_num_fail":32, // 2^(2*slot_index+1) 失败时,要将『低第六位』置为1,同时确保『低第五位』为0
"depends":["TextUnderstanding","WidgetInit"] // 文本处理组件执行,依赖文本理解与插件选择组件执行完成
},
……
{
"component_name" : "FootageGenerator", // 素材生成组件
"slot_index":3,
"slot_num_success":64,
"slot_num_fail":128,
"depends":["TextUnderstanding","WidgetInit","TextProcessor"] // 依赖前面三个组件
},
{
"component_name" : "MaterialSearch", // 素材检索组件
"slot_index":4,
"slot_num_success":256,
"slot_num_fail":512,
"depends":["TextUnderstanding","WidgetInit","TextProcessor"] // 也只依赖前面三个组件
},
……
]
},
{ // 视频生成模块
"module_name":"VideoGenerator",
"status":"generating",
"next_status":"draft",
"components":[
{
"component_name" : "VideoRender",
"slot_index":7,
"slot_num_success": "2^14",// 2的14次方
"slot_num_fail":"2^15",// 2的15次方
"depends":[""] // 在当前模块内,没有前置依赖
}
]
}
……
}
流程描述文件的组织逻辑为:
基本描述单元为组件,说明组件在流程中所在位次与对应的槽位值、组件执行的前置依赖组件
每个组件只关注自身执行所需关键信息,不关注其他组件的执行逻辑
在同一个模块内的组件,组合成为模块单元,模块单元关注当前模块状态,以及当前模块执行完毕之后的下一个状态
所有模块按照执行顺序(因为模块是绝对串行的)组织成完整流程描述文件
后续的整体流程调度,将以该文件为蓝本执行。同时,可以看到,一个描述文件即规定了一个流程,如果我们有不同的业务场景需要不同的执行流程,那么只需要再编排一个流程调度文件即可,事实上,我们的AIGC 业务也确实存在多条流程,整体编排逻辑同理,不多赘述。
4.3 流程调度
服务编排框架的核心,是流程调度部分,该部分负责维持与推动数据流的运转。如上文所述,每个组件的状态都通过相对应的位值来维护,流程调度的关键就在于对位值的管理。整体流程如图4所示,整个流程调度通过消息队列串联,主要操作步骤如下:
①任务创建:该步骤在一个任务执行全流程中只执行一次,主要在前置的参数检查校验工作完成之后,进行数据的入库操作,并将任务下发流程调度消息队列,触发整体流程。
②查找可执行组件并执行:该步骤在一个任务执行全流程中会执行多次,在正常情况下,与组件个数等同。该步骤主要负责从消息队列中拉取数据,遍历流程描述文件,通过计算当前任务的调用/返回槽位值,推算出各个组件执行状态,若某个组件未执行、且其依赖的前置组件已执行完毕,则将该组件加入执行队列;若未找到可执行组件,则本次不执行。在这一步中,若组件内部存在异步微服务,则仅作微服务触发,若为同步组件,则会在执行完毕之后,将任务再次加入流程调度消息队列。
③异步回调:我们大部分组件都是异步微服务,故而在第二步中触发微服务调用之后,这一环节主要功能是接收微服务回调,并做相关后置业务处理,处理完成之后,再将任务再次加入流程调度消息队列。
△图7 任务调度流程图
在这个流程里我们通过消息队列的调度解耦了组件之间的相互依赖,仅通过槽位值查询与校验来实现流程的流转与执行,这使得系统具备了组件的并发性,只要定义好每个组件执行的前置依赖,那么当一个组件执行完成之后所有依赖这个组件的后置组件都可以开始执行。那么,这时候会出现另外一个问题,我们如何保证并行执行完成之后的槽位值更新不彼此覆盖?如果两个组件同时执行完成,但每个组件只会计算并修改自身槽位值,如何保证后更新的槽位值不覆盖前一个组件的槽位值?这个问题的解决我们是通过利用消息队列的重试做后置更新结合更新锁来完成的:在每个组件执行完成之后只会更新自身涉及的业务字段,而不更新状态及槽位值,状态管理的三个值是在步骤二中前置执行的,每次从消息队列中拉取一个任务后会先进性状态的检查和槽位值的更新,更新前会先加唯一锁,若加锁失败则可能其他组件正在做状态更新,则退出执行,该任务依然在消息队列里未消费,待下一次继续执行。
采编式 AIGC 视频生产流程2022年5月上线以来,已经根据不同的业务场景,通过对基础模块和组件的组合配置建设起5条不同的生产流程,很好地支持万级日产的业务发展。随着业务的迭代深入,相关组件的功能及代码量都在日益膨胀,我们成功地在当前框架下进行组件的拆分与扩展,在不触动底层调度框架基础上,安全高效地完成了组件的扩展。虽然当前框架对目前的业务支持良好,但是整个流程的优化和迭代还在继续,对成熟服务编排引擎的调研也在继续,希望后续在借鉴成熟框架的基础上,能够沉淀出更为稳定高效的视频生产流程。
——END——
参考资料:
[1]https://cadenceworkflow.io/docs/get-started/
[2]https://docs.temporal.io/temporal/
[3]https://conductor.netflix.com/architecture/overview.html
[4]https://netflixtechblog.com/netflix-conductor-a-microservices-orchestrator-2e8d4771bf40
推荐阅读:
百度工程师漫谈视频理解
百度工程师带你了解Module Federation
巧用Golang泛型,简化代码编写
Go语言DDD实战初级篇
Diffie-Hellman密钥协商算法探究
贴吧低代码高性能规则引擎设计