我英文差, 但是又不能只看中文(讲话的人是英文,失去原味),所以打算双字幕观看。英文字幕在 mpv 播放器下方,看到不懂的字可以望上面的中文字幕。
虽然此视频 IlU-zDU6aQ0 网页版有英文字幕以及自动翻译的中文字幕,可是旧版本的 youtube-dl 和随便两个网站 (https://downsub.com 和 https://savesubs.com) 都只能拿到英文字幕。
虽然最新版本的 youtube-dl 已经 fix 了,无论如何,要手动完成此任务,也行的:
1. 下载英文字幕 (.vtt): youtube --skip-download --write-sub --sub-lang en https://www.youtube.com/watch?v=IlU-zDU6aQ0 。
2. 在 Youtube 播放器, 变改语言边 inspect network, 得到 `api/timedtext?` 链接。
用 curl 跑链接获得 .json 格式的字幕。
3. 转换 .json 去 .srt 不需要重造轮子,浏览 https://zhuanlan.zhihu.com/p/337934938 下载某人写的 node.js 代码, `parse_subtitle.js`, 加上这三条 line:
let sourcePath = process.argv[2]
let resultPath = process.argv[3]
let isTime = true
4. 然后把这代码: `s.segs[0].utf8` 改成 `s.segs[0].utf8.match(/.{1,21}/g).join('\n')` ,
以便能在 mpv 播放器, 每 21 个中文字 wrap,避免一行长过播放器宽度,因为 mpv 的 libass 不支持没空白的中文字 wrap。
5. 运行 `node parse_subtitle.js IlU-zDU6aQ0.sub.zh-Hans.json IlU-zDU6aQ0.sub.zh-Hans.srt` ,
把 .json 转换去 .srt
6. 运行 `mpv --sub-file="IlU-zDU6aQ0.sub.zh-Hans.srt" --sub-file="Marty Lobdell - Study Less Study Smart-20110722-IlU-zDU6aQ0.en.vtt" --sid=2 --secondary-sid=1 'Marty Lobdell - Study Less Study Smart-20110722-IlU-zDU6aQ0.mp4` ,
播放 .srt 中文字幕在上方, 以及 .vtt 英文字幕在下方。
youtube-dl fix 了不表示这 web API 就没用,其实两 API 的翻译是有出入的。
web API 是 53 个中文字翻译(我切为三行,但翻译是一起的),而 youtube-dl 是 27 个中文字翻译。
理论上 53 一行肯定比 27 一行能翻译比较准确,
不过两者都没考虑上下文的句号来断句翻译, 所以翻译可能怪怪的。
当然,句号翻译的前提是原文是有句号的,
否则没句号却强行变成同一行翻译也会有问题。
youtube-dl 原字幕(上图), ,web API 自动中文翻译(左下), youtube-dl 自动中文翻译(右下):
我也曾尝试 Android 11 的 Live Caption (即时字幕) 功能以达到类似效果。
可是 Live caption当前只有英文:
,且 player 的该视频只有英文选项。
只要其一有中文即可(不过如果只能选 player 自动翻译的中文,意味着两个都是机器翻译,准确性也大打折扣),偏偏两个都是英文,任务失败。
Android Live caption + 视频字幕效果图, Live caption (移去上方), 英文原字幕 (下方):
不过桌面版的 Chrome 也有 Live Caption 啊!
只不过必须安装 Chrome Canary(否则就算启动设置也是没用的)
,可是 Linux 却没有 Chrome Canary(除非... 用 wine/虚拟机... 囧我不奉陪了)。
算了,试下打开封尘不懂几年的 Windows。
安装好 Chrome Canary 后,去 `chrome://flags/#enable-accessibility-live-captions`(没有出现 "Live Caption" 就搜 "Live" 关键字) 设置 "Enabled" 它。
(Android 的 Chrome Canary 没有该设置... Android 再次任务失败)
Enabled 后下面提示你 "Relaunch" Chrome。
重启 Chrome 后,然后去 settings -> accessibility 启动 Live Caption
,抑或去视频页面的右上方 "≡♪" 图标会能给你实时启动/关闭 Live Caption。
第一次启动 Live Caption 后会花一些时间下载语言分析包, 不过当前只有英文所以不会下载太久。
然后就完成啦。视频播放器字幕没办法永久移动, 所以只能移动 Live caption 字幕去上方。
由于 Live caption 只有英文,那么中文的就必须是播放器字幕,就形成了英文必须在上方的情况
(必须全屏,否则头被完全盖着囧
那个占位的箭头有点鸡肋,展开又太多行眼花缭乱,不展开又嫌少行, 也不能自定义多少行):
无论如何,Live Caption 用 speech 翻译的准确性肯定比原文翻译低。
如上图, "Ontogeny" 变成 "On to me"。
当然,播放器的自动翻译也无法保证百分百,但起码能比 speech 翻译准确。
除非本来就没字幕,就可能打平, 但实时翻译真的能比得上预先翻译?
Live caption 还有说了该段话才显示的局限性
(无法一次性理解整体, 当然你也可以说有利弊,可以了解当前讲的话是哪一个词)
, 些许延迟(我只观察一下)。
最后,避免日后知乎删除 .json 转 .srt 的代码文章,所以备份在这 (//orig 注释表示原版, //hole 表示我改的):
let sourcePath = process.argv[2] //'IlU-zDU6aQ0.sub.zh-Hans.json'
let resultPath = process.argv[3] //'IlU-zDU6aQ0.sub.zh-Hans.srt'
let isTime = true
//orig: https://zhuanlan.zhihu.com/p/337934938
var fs = require('fs')
//2. 处理 json
fs.readFile(sourcePath, 'utf8', function (err, data) {
if (err) console.log(err);
var test1 = JSON.parse(data);//读取的值
// console.log(test1.events)
subtitle = test1.events
if (fs.existsSync(resultPath)) {
fs.writeFileSync(resultPath, '')
}
for (let i = 0; i < subtitle.length; i++) {
const s = subtitle[i];
// console.log(s.segs[0].utf8) //字幕内容
// i :字幕的位置
// console.log(s.tStartMs) 起始时间
// console.log(s.tStartMs + s.dDurationMs) 结束时间
// 字幕格式
// 1
// 00:00:09,779 --> 00:00:15,899
// 内容
// 空行
if(isTime){
fs.appendFileSync(resultPath, i + '\n' + convertTime(s.tStartMs) + ' --> ' + convertTime(s.tStartMs + s.dDurationMs)
//+ '\n' + s.segs[0].utf8 + '\n\n',err => { //orig
+ '\n' + s.segs[0].utf8.match(/.{1,21}/g).join('\n') + '\n\n',err => { //hole
if(err){
console.log(err)
}
})
}else{
fs.appendFileSync(resultPath, s.segs[0].utf8 + '\n\n',err => { //orig
if(err){
console.log(err)
}
})
}
}
});
//3. 处理时间的方法
function convertTime(time) {
var s, m, h
h = Math.floor(time / 1000 / 60 / 60)
console.log('h:' + h)
m = Math.floor(((time / 1000 / 60 / 60) % 1) * 60)
// console.log('m:' + m)
s = ((((time / 1000 / 60 / 60) % 1) * 60) % 1) * 60// 通过与 1 取余获取小数部分
s = s.toFixed(3)
s = s.toString().replace('.', ',').substring(0, 6)
console.log('s:' + s)
if (h < 10) {
h = '0' + h
}
if (m < 10) {
m = '0' + m
}
var timeString = h + ':' + m + ':' + s
// console.log(timeString)
return timeString
}