06月22, 2018

实现全文机器翻译

众成翻译三期计划实现全文机器翻译功能,背后的需求是把长期未被译者认领的文章,通过谷歌高级翻译API自动翻译出来,然后人工编辑、审校后发布。这样一方面可以消化累积的原文,另一方面也能增加产出。

乍一看,实现自动全文翻译,无非就是把文章以段为单位发给翻译API,拿到译文后再逐段替换回去就行了。

其实不然。

并非所有内容都要翻译,比如代码。代码有代码段和文本内的(行内)代码,都不能翻译。因此简单以段为单位翻译替换的想法行不通。

不仅如此,原文是Markdown格式,所以文本中的网址、图片地址也不能走API,但是链接文本和图片说明又必须翻译。比如

[Please visit this link](http://www.very-good-domain-name.com/how-to-master-react-app-development.html)

这个链接,“Please visit this link”要翻译,但“very-good-domain-name”和“how-to-master-react-app-development”不能翻译。

细思恐极。怎么办?只能借助Markdown解析和编译工具,比如marked,它能把Markdown转换成HTML。关于marked作为编译器的结构和原理,可以参考这篇文章:探究JavaScript上的编译器 —— marked。下面关于marked的架构图也摘自这篇文章:

marked的架构

经分析,marked有两个切入点可资利用。本文先介绍利用第一个切入点的方案,而且这个切入点也正好能满足前述需求分析。

marked的第一个切入点是Renderer的text()方法:

Renderer.prototype.text = function(text){
   return text; 
}

据观察,作为“span level renderer”的text()负责处理所有文本。换句话说,需要翻译所有内容都要经过它,而且不需要翻译的内容不会经过它!简直完美,哈哈。

这么说来,只要在调用marked的时候重写text(),把它接收到的每段英文翻译成中文再返回就可以了,如下面的伪代码所示:

// 创建自定义的Renderer对象
let renderer = new marked.Renderer()

renderer.text = function(text){
   //调用谷歌高级翻译API翻译
   let translation = googleTranslationPremium.translate(text,options)
   return translation; 
}

然而不行!marked是一个同步库,而调用API取得结果是个异步过程,这样注入的并不是异步调用的返回结果,而是“[object Promise]”这个字符串!(谷歌高级翻译支持Promise方式调用。)

怎么办?可以把marked重写为异步版,太花时间(或许以后有时间可以考虑)。一个变通方案是“用空间来换时间”:就是运行两遍marked,第一遍只收集英文text,然后在marked外部调用API完成翻译,第二遍再利用翻译结果同步替换英文text

代码如下:

// 取得文章MD文本
let mdString = (await this.model('article').where({id:2254}).find()).content
// 全文翻译对象,以键-值对形式保存每段英文和译文
let translations = {}

// 创建自定义渲染器
let renderer = new marked.Renderer()

// 第一遍:获取文本
renderer.text = function(text) { 
  // 初始化全文翻译对象,把每段文本的英文作为对象的键
  translations[text] = ''
  return text
};
// 无需保存结果
marked(mdString,{renderer:renderer})

// 中间处理:异步翻译
for(let text in translations){
  // 限制请求频率,每秒发送一次翻译请求
  await sleep(1)
  // 发送翻译请求,异步得到每段英文的译文
  let translation = await googleTranslationPremium.translate(text,options)
  // 取得译文,并作为全文翻译对象对应键的值
  translations[text] = translation[0]
  // 监控实时输出
  console.log(translation[0])
}

// 第二遍:生成HTML
renderer.text = function(text) {
  // 把英文替换为译文
  text = translations[text]
  return text
};
let html = marked(mdString,{renderer:renderer})    

这个方案成功了,自动翻译结果如下:

Image

但新问题又暴露出来了。比如这句英文:

See Vue.js’ installation page for more info.

因为中间有一个链接,所以在通过text()方法时,会分三次调用,分别翻译:

  1. See
  2. Vue.js’ installation page
  3. for more info.

翻译结果组合起来就是:

看到Vue.js“安装页面 更多信息。

本来是完整的一句话,硬分成三个片段分别翻译,理论上会丢失上下文,翻译结果应该不是最佳的。比如,把这句话完整地发给谷歌高级翻译,得到的结果是:

有关详细信息,请参阅Vue.js的安装页面。

显然好多了。

为此,还要利用marked第二个可以利用的切入点,尝试解决这个问题。想知道marked的第二个切入点是什么,如何满足需求同时解决问题?敬请期待下一篇文章。

关于这个方案的代码,需要补记两个问题,请读者注意:

  1. 前面示例代码中的await sleep(1),并非Node.js内置的方法;
  2. 方案的完整实现应该包括错误补偿,即加上所谓的“指数退避”策略

本文链接:http://lisongfeng.cn/post/full-text-machine-translation-in-zcfy-cc.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。