背景
笔者最近看了《致命女人》这部美剧,觉得台词都很贴近生活,对话也经常抖包袱,很有必要再回味一遍。可是一集40分钟,哪有那么多时间呢?而且,怎么样才能最快定位到自己想要找的精彩台词呢?一集240M的大小放在手机上也十分占用空间,而且自己的笔记都是放在iPad上。面对诸多不便,是时候展示一下程序员的倔强了。
过程
最简单的办法就是看剧本(或者字幕文件),但是作为一个学习能力一般的人,最起码的图文并茂还是要有的,于是脑海中出现了一个想法
把字幕对应的截图一一保存下来,整理成一个PDF不就行了。这样既可以通过视觉加强回忆,也可以通过文本快速搜索。wonderful!
首先查阅了网上提取字幕的方法,大多是OCR。因为视频里面的字幕都是"硬字幕"(即通过视频编辑软件写进去的,无法逆向提取出来,当然像mkv这种格式是可以的,但人人影视上下的是mp4,硬字幕实锤了),所以这种想法很自然,但也有几个问题:
速度慢(显然)
容易被噪音干扰(可能一帧有广告标语的画面也被截进来了)
不能保证100%准确
当然最重要的一点是,我觉得一晚上搞不定,所以放弃了这个策略。换个思路,能不能先拿到字幕,然后计算出每一条字幕出现的时间,去视频指定位置截图就OK了。首先找到了字幕
下载之后发现格式较为简单的 srt
文件全部乱码了,只好硬着头皮去处理带了文字格式、较为复杂的 ass
文件,打开之后大概是这样:
......
Dialogue: 0,0:00:08.36,0:00:13.74,*Default,NTP,0,0,0,,{\an8\fscx120\fscy131\move(183.56,27.946,188.94,26.346,15,4000)}{\fs38\fsp8\fn方正兰亭特黑长简体\b1\bord0\blur0\shad6\fad(0,100)\c&H43D4E1&}{\fs50}{\t(15,3600,\fs16)}致命女人
Dialogue: 0,0:00:08.36,0:00:13.74,*Default,NTP,0,0,0,,{\an8\fscx120\fscy131\move(183.56,133.546,185.74,67.413,15,3600)}{\fs28\fsp8\fn方正兰亭特黑长简体\b1\bord0\blur0\shad6\fad(0,100)\c&H43D4E1&}{\fs28}{\t(15,3600,\fs14)}第一季 第一集
Dialogue: 0,0:01:12.55,0:01:15.33,*Default,NTP,0,0,0,,{\an8\pos(194.4,101.333)}{\fs28\fsp2\fn方正小标宋简体\b1\bord0\blur0\shad0\fad(50,0)}丈夫
Dialogue: 0,0:01:15.94,0:01:18.03,*Default,NTP,0,0,0,,我和贝丝·安高中时开始交往\N{\fn微软雅黑}{\b0}{\fs14}{\3c&H202020&}{\shad1}I started dating Beth Ann in high school.
Dialogue: 0,0:01:18.45,0:01:22.35,*Default,NTP,0,0,0,,她以前常给我做三明治还有缝衣服扣子\N{\fn微软雅黑}{\b0}{\fs14}{\3c&H202020&}{\shad1}She used to make me sandwiches and sew buttons on my shirts.
Dialogue: 0,0:01:23.17,0:01:25.00,*Default,NTP,0,0,0,,相信我 爱照顾人的女生\N{\fn微软雅黑}{\b0}{\fs14}{\3c&H202020&}{\shad1}I tell you, there's nothing sexier than a girl
Dialogue: 0,0:01:25.00,0:01:27.01,*Default,NTP,0,0,0,,最性感了\N{\fn微软雅黑}{\b0}{\fs14}{\3c&H202020&}{\shad1}who likes to take care of you.
Dialogue: 0,0:01:27.99,0:01:30.01,*Default,NTP,0,0,0,,我是在一次慈善晚会上认识萨蒙妮的\N{\fn微软雅黑}{\b0}{\fs14}{\3c&H202020&}{\shad1}I was introduced to Simone at a benefit.
Dialogue: 0,0:01:30.01,0:01:32.51,*Default,NTP,0,0,0,,她的出场太惊艳了\N{\fn微软雅黑}{\b0}{\fs14}{\3c&H202020&}{\shad1}Oh, the entrance she made.
......
我们需要的只有3列,开始时间、结束时间和字幕,于是文本处理开始,截取代码如下
dos2unix ${originSubtitle}
grep -F "Dialogue" ${originSubtitle} > ${tag}
sed -i "s/{.*}/|/g" ${tag}
sed -i "s/\\\\N|/,/g" ${tag}
sed -i "s/|//g" ${tag}
cat ${tag}| cut -d "," -f 2,3,10,11 > tmp.txt
cat tmp.txt > ${tag}
rm tmp.txt
dos2unix ${tag}
然后就拿到了一下文本
0:00:08.36,0:00:13.74,第一季 第一集
0:01:12.55,0:01:15.33,丈夫
0:01:15.94,0:01:18.03,我和贝丝·安高中时开始交往,I started dating Beth Ann in high school.
0:01:18.45,0:01:22.35,她以前常给我做三明治还有缝衣服扣子,She used to make me sandwiches and sew buttons on my shirts.
0:01:23.17,0:01:25.00,相信我 爱照顾人的女生,I tell you
0:01:25.00,0:01:27.01,最性感了,who likes to take care of you.
0:01:27.99,0:01:30.01,我是在一次慈善晚会上认识萨蒙妮的,I was introduced to Simone at a benefit.
0:01:30.01,0:01:32.51,她的出场太惊艳了,Oh
0:01:32.95,0:01:35.46,设计师礼服 挂满钻石,Designer gown
....
由于需要计算字幕的中间时间,需要先转化成毫秒,然后再转回来,考虑到 shell
是我用过最垃圾的语言之一,并且 不善计算和字符串处理,我一度考虑要不要专门写个子程序做这件事,但考虑到其他语言也不是很精通,只能又硬着头皮上了
# use lua is a lazy way
lua="lua5.3"
# hh:mm:ss.xxx to millisecond
# return (hh * 3600 + mm * 60 + ss) * 1000 + xxx
function hhmmss2millisecond() {
originTime=$1
hh=$(echo ${originTime} | cut -d ":" -f 1)
mm=$(echo ${originTime} | cut -d ":" -f 2)
ss=$(echo ${originTime} | cut -d ":" -f 3 | cut -d "." -f 1)
xxx=$(echo ${originTime} | cut -d ":" -f 3 | cut -d "." -f 2)
xxx=$(echo "0."${xxx})
${lua} -e "print( (${hh} * 3600 + ${mm} * 60 + ${ss}) * 1000 + ${xxx} * 1000 )"
}
function millisecond2hhmmss() {
originTime=$1
result=$(${lua} -e "print(math.modf(${originTime}/ 1000))")
second=$(echo ${result} | cut -d " " -f 1)
xxx=$(echo ${result} | cut -d " " -f 2 | cut -d "." -f 2 | cut -c 1-3)
# ss=$(${lua} -e "print(${second}%60)")
ss=$(${lua} -e "print(math.fmod(${second}, 60))")
second=$(${lua} -e "print(${second} - ${ss})")
result=$(${lua} -e "print(math.modf(${second} / 60))")
minute=$(echo ${result} | cut -d " " -f 1)
mm=$(${lua} -e "print(math.fmod(${minute} , 60))")
hh=$(${lua} -e "print(math.modf(${minute} / 60))")
hh=$(echo ${hh} | cut -d " " -f 1)
echo ${hh}":"${mm}":"${ss}"."${xxx}
}
...
cnt=0
# cost much time
while read -r line; do
cnt=$((${cnt} + 1))
start=$(echo "${line}" | cut -d "," -f 1)
end=$(echo "${line}" | cut -d "," -f 2)
start=$(hhmmss2millisecond ${start})
end=$(hhmmss2millisecond ${end})
middle=$(${lua} -e "print( (${start} + ${end})/2 + ${offset}*1000 )")
pos=$(millisecond2hhmmss ${middle})
arr[${cnt}]=${pos}
echo -ne "\rprocess ${cnt} / ${lineCount}"
done < "${tag}"
这样我就拿到了每条字幕出现的时间了,这里有两点需要注意下:
offset
: 由于前面插入了广告,所以字幕文件和实际字幕出现的位置会有一个偏移不能把截图时间写在一个文件里面,然后在
while read
循环里面直接调用ffmpeg
,不然会出现以下错误
Invalid duration specification for ss: :2:6.135
也就是 :
前面的内容被吞了,但是注释掉这行就不会,思考之后发现是 ffmpeg
会影响 read
,也就是说 while
循环里面的操作会影响他自己,造成预期外的结果,我当时真的佛了,大约有半个小时,我发现只要用 echo
输出,所有的行都是合法的,但是只要调用了了 ffmpeg
读到的文件内容竟然能变,在没有任何代码显式操作的情况下,一度以为撞鬼了。一个小插曲,明白过来以后,我把它存在一个数组里面,然后再单独处理数组,避免 ffmpeg
和 read
的冲突
cnt=0
for i in ${arr[@]} ; do
cnt=$((${cnt} + 1))
echo ${i}
ffmpeg -ss ${i} -i ${originVideo} -vframes 1 -vf "scale=600:600/a" -q:v 10 ${cnt}.jpg
done
注意几个细节
-ss
:放在前面,可以节省运行时间,尤其是大文件-q:v
:不要太低,否则会让图片质量变大(设置前生成的pdf有60M,设置后只有10M)-vf "scale=600:600/a"
: 缩放,图片没必要太大
最终效果
最后把这些图片用通过markdown转成pdf就行了
out="out.md"
echo "" > ${out}
while read -r line; do
cnt=$((${cnt} + 1))
echo "" >> ${out}
echo "" >> ${out}
echo ${line} | cut -d "," -f 3- >> ${out}
echo "" >> ${out}
echo -ne "\rprocess ${cnt} / ${lineCount}"
done < "${tag}"
exit
这里一开始想用 pandoc
, 结果各种 latex 依赖需要处理,于是干脆转成 html,再用谷歌浏览器保存成pdf
后面如有时间,打算通过这种方式把权游和《破产姐妹》好好学习下!!!