Sorry… 我也用代码玩玩这个梗

前言

自从几年前在变形计中“自打脸”的王境泽火了以后,就陆续传出各种修改字幕后的恶搞系列表情包。起初还是以图片为主,后来就变成视频和 GIF,再到现在,王境泽、偷车贼窃格瓦拉、为所欲为等“明星”并驾齐驱……

我说哪有这么人闲得无聊啊,制作这么多的 GIF。原来啊是出现了各种生成器,所以大大降低了制作这些东西的成本。所以呢,最近我也抱着好玩的心态尝试打算用代码来生成这些东西,于是便有了本文。

说明

我们将使用 Go 语言,利用 ffmpeg 生成相应资源。具体原理:

  1. 准备无字幕的原视频和字幕模板
  2. 根据参数(手动输入的句子)生成最终字幕
  3. 合并字幕和原视频,输出为 GIF 或者 VIDEO

先看个最终效果:

生成器前端在这里:https://sorry.bluerain.io

如果对视频处理有过一定研究的肯定知道 ffmpeg 是什么了。即便说你没有相关经验,在 Linux 平台上听过它也不奇怪。例如 mpv 播放器的在线播放功、you-get/youtubhe-dl 的视频合并功能等众多涉及视频音频的软件都会使用 ffmpeg……

对 ffmpeg 的补充介绍(摘自 Wiki):

FFmpeg是一个自由软件,可以运行音频和视频多种格式的录影、转换、流功能,包含了libavcodec(一个用于多个项目中音频和视频的解码器库),以及libavformat(一个音频与视频格式转换库)。

简单来说,ffmpeg 可以为我们这个项目提供视频和字幕的合并功能以及视频到 GIF 的转换功能。

过程

首要我们要实现根据输入参数利用模板动态生成最终字幕的功能,准备一个为所欲为(sorry)的原始字幕:

[Script Info]
; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/
Title: 为所欲为
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.601
PlayResX: 300
PlayResY: 168

[Aegisub Project Garbage]
Audio File: template.mp4
Video File: template.mp4
Video AR Mode: 4
Video AR Value: 1.781250
Video Zoom Percent: 2.000000
Active Line: 8
Video Position: 25

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: sorry,WenQuanYi Micro Hei,23,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1.1,0.5,2,5,5,5,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.18,0:00:01.56,sorry,,0,0,0,,好啊
Dialogue: 0,0:00:03.18,0:00:04.43,sorry,,0,0,0,,就算你是一流程序员
Dialogue: 0,0:00:05.31,0:00:07.43,sorry,,0,0,0,,写出来的代码再完美
Dialogue: 0,0:00:07.56,0:00:09.93,sorry,,0,0,0,,我说这是 BUG 它就是 BUG
Dialogue: 0,0:00:10.06,0:00:11.56,sorry,,0,0,0,,毕竟我是用户
Dialogue: 0,0:00:11.93,0:00:13.06,sorry,,0,0,0,,你害我加班啊
Dialogue: 0,0:00:13.81,0:00:16.31,sorry,,0,0,0,,sorry 我就喜欢看程序猿加班
Dialogue: 0,0:00:18.06,0:00:19.56,sorry,,0,0,0,,以后天天找他 BUG
Dialogue: 0,0:00:19.60,0:00:21.60,sorry,,0,0,0,,天天找 天天找

将 [Events] 里边的每一行 Dialogue 嵌入模板相关标签:

# ……
Dialogue: 0,0:00:01.18,0:00:01.56,sorry,,0,0,0,,{{ index .sentences 0 }}
Dialogue: 0,0:00:03.18,0:00:04.43,sorry,,0,0,0,,{{ index .sentences 1 }}
Dialogue: 0,0:00:05.31,0:00:07.43,sorry,,0,0,0,,{{ index .sentences 2 }}
Dialogue: 0,0:00:07.56,0:00:09.93,sorry,,0,0,0,,{{ index .sentences 3 }}
Dialogue: 0,0:00:10.06,0:00:11.56,sorry,,0,0,0,,{{ index .sentences 4 }}
Dialogue: 0,0:00:11.93,0:00:13.06,sorry,,0,0,0,,{{ index .sentences 5 }}
Dialogue: 0,0:00:13.81,0:00:16.31,sorry,,0,0,0,,{{ index .sentences 6 }}
Dialogue: 0,0:00:18.06,0:00:19.56,sorry,,0,0,0,,{{ index .sentences 7 }}
Dialogue: 0,0:00:19.60,0:00:21.60,sorry,,0,0,0,,{{ index .sentences 8 }}

因为输入参数是一个字符串数组,所以直接使用 index 函数取数组对应的下标的值即可。

接着我们进行一个基本的实现:

注意:下面的 Go 代码考虑到阅读的直观性和简洁性,不会做任何的错误处理。

func makeAss(tplContentText string, fWriter *io.Writer) {
	sentences := []string{
		"好啊",
		"就算你是一流程序员",
		"写出来的代码再完美",
		"我说这是 BUG 它就是 BUG",
		"毕竟我是用户",
		"你害我加班啊",
		"sorry 我就喜欢看程序猿加班",
		"以后天天找他 BUG",
		"天天找 天天找",
	}
	data := map[string][]string{
		"sentences": sentences,
	}
	tpl := template.New("subTitle")
	tpl, _ = tpl.Parse(tplContentText)
	tpl.Execute(fWriter, data)
}

传递给此函数字幕模板的内容和输出文件的 File 对象即可:

func TestTplConvContent(t *testing.T) {
	assBuf, _ := ioutil.ReadFile("./resources/template/sorry/template.ass")
	outputAss, _ := os.Create("./dist/output.ass")
	makeAss(string(assBuf), outputAss)
}

执行这个 test 函数,dist 目录会生成 output.ass,内容即为最终字幕。

最终字幕的生成其实就是一个很基础的模板用法,所以非常的简单,不难理解。而视频字幕的合并听起来很复杂,但是如果看看下面的 ffmpeg 的使用方法就会觉得非常的简单(容易)了。

直接使用 ffmpeg 合并字幕和视频文件:

ffmpeg -i template.mp4 -vf ass=dist.ass -an -y output.mp4

需要注意的是你所使用的 ffmpeg 必须包含 libass,不然可能会出现找不到 assFilter 的情况。

附加解释:-an 参数的作用是去掉输出视频的音频内容。

现在我们需要做的就是用 Go 代码完成对上述命令的包装,在最终字幕生成以后作为 dist.ass 参与包装函数的运行即可:

func makeVideo() {
	cmd := exec.Command("ffmpeg", "-i", "./resources/template/sorry/template.mp4",
		"-vf", fmt.Sprintf("ass=%s", "./dist/output.ass"),
		"-an",
		"-y", "./dist/output.mp4")
	cmd.Start()
}

在执行完 makeAss 以后再执行 makeVideo 便可看到输出后的视频文件,它包含了输入的字幕,其实就核心功能已完成了,剩下的就是对程序的设计和代码的优化。

数数可以发现其实也就几十行代码而已,非常的简单:)所以在有现成工具可以利用的情况下,有时候看起来很复杂的东西实际上很简单就能 Programmably 化。

结束语

当然,我的项目比这篇文章的示例代码要复杂多了,并且已经比较完善了,欢迎大家【Star】。

最后,这是一篇毫无技术含量,纯粹为了好玩而已的文章……