Thursday, 25 February 2021

中文和英文双字幕观看 YouTube 视频

我英文差, 但是又不能只看中文(讲话的人是英文,失去原味),所以打算双字幕观看。英文字幕在 mpv 播放器下方,看到不懂的字可以望上面的中文字幕。

虽然此视频  IlU-zDU6aQ0 网页版有英文字幕以及自动翻译的中文字幕,可是旧版本的 youtube-dl 和随便两个网站 (https://downsub.comhttps://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,避免一行长过播放器宽度,因为 mpvlibass 不支持没空白的中文字 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
}





No comments:

Post a Comment