LibreTranslate本地部署及简单封装其API的Android APP设计

话说现在很多在线翻译服务都对字符长度和频率有限制,API更是如此,非常恶心,于是就想着自己在本地搭建一个翻译服务。去GitHub上找了一圈,确实有这种玩意,那就是LibreTranslate,它的核心库ArgosTranslate是基于OpenNMT的神经网络翻译模型的。LibreTranslate是可以完全自主托管的,部署后除了可以在弹出的浏览器中在线翻译,还可以调用其提供的API来设计专有app进行翻译。但毕竟它的核心是基于神经网络的,需要pytorch来支持,因此有GPU加速的话翻译速度会快很多。

一、基础环境搭建

网上很多教程都是用docker搭建虚拟容器的,而且要Linux支持,感觉比较麻烦,因此我直接在Windows上采用anaconda加pytorch-cuda的方式部署,个人觉得相对简便些。下面来说一下步骤(前提是你的计算机上面已经安装好了NVIDIA显卡驱动和CUDA运行环境):

1. 下载Anaconda,并安装,官网:Free Download | Anaconda

 

 2. 进入Anaconda,选择左侧的Environments,再点击Create,新建一个Python虚拟环境,随便输入一个名字,package选择默认的Python3.9即可,再点击一次Create就OK了。

3. 选择刚刚创建好的虚拟环境,点击运行按钮——Open Terminal,然后会弹出命令提示符窗口。

 4. 接下来在命令提示符中依次输入以下命令,安装依赖包和LibreTranslate本体:

pip install --no-cache-dir torch==1.12.0+cu116 -f https://download.pytorch.org/whl/torch_stable.html

pip install libretranslate

5. 安装完毕后,依次输入以下命令启动LibreTranslate后台服务:

set ARGOS_DEVICE_TYPE=cuda
libretranslate --load-only de,en,es,fr,it,zh --host 0.0.0.0 --port 5000

这里要说明一下,第一条命令是告诉libretranslate要使用CUDA加速的,第二条命令是直接启动服务的,其中第二条命令的 --load-only 参数是用来指定加载哪些语言模型的(也就是可以翻译哪些语言),不指定则默认加载所有语言模型。这里选取你最常用的翻译语言就行,就我而言,我选择的是德语、英语、西班牙语、法语、意大利语和中文。--host 参数是指定绑定到哪个IP的,这里选择0.0.0.0,保证本机和局域网内所有设备都能访问;--port 就是指定端口号的,默认是5000。

然后就可以在本地浏览器打开 http://localhost:5000 进入翻译页了。下面测试一下翻译效果:

首先是西班牙语和英语之间的翻译:

嗯,看样子洋文之间翻译的效果还不错。

然后看看英语和中文之间的翻译:

可以说是惨不忍睹,很多内容都没翻译过来,句子也是狗屁不通,没有逻辑和连贯性。只能说中文翻译模型的语料库可能不够,或者还有待优化,现在就只能当个玩具用用了。

而且还有一点,由于很多语言是只和英语之间有直接相互翻译的关系,那么其他非英语的语言之间互相翻译效果可能就会差很多,比如西班牙语->中文,系统是先将西语翻译为英语,再将英语翻译为中文。所以西语翻译为中文的效果估计是更惨不忍睹。

鉴于现在的情况,LibreTranslate这个平台比较适合英语水平不错但需要大量阅读非英语文献的人使用。若想自然流畅地翻译中文,现在只能找别的收费替代品了。

二、API封装及对应的Android客户端开发

虽说翻译质量不怎么样,但毕竟是免费无限制的,可以在本地部署,而且在网页的下方它还提供了API的使用示例:

那么何不开发一个Android端APP,在家免费享受无限次翻译呢?现在就开始动手,这次我打算用简便的Kotlin语言编写代码。

首先分析API的格式,这个API的地址是http://<ip>:<port>/translate,接受的请求为POST类型,数据类型为Json,服务器翻译完毕后回复的内容数据类型也是Json。那么首先定义两个数据类TransRequest和TransResponse,分别封装这两个Json数据的所有内容,代码如下:
 

data class TransRequest(val q: String, val source: String, 
    val target: String, val format: String, val api_key: String)

data class TransResponse(val translatedText: String)

然后就是向服务器发送请求并接受响应数据了,这里我们采用Retrofit网络库来实现,它是对OkHttp的上层接口封装,使得我们可以用更加面向对象的思维进行网络操作。要使用Retrofit,首先要定义一个封装API的接口,传入API的相对路径即可,代码如下:

import retrofit2.*
import retrofit2.http.*

interface ApiService {
  @Headers("Content-Type: application/json")
  @POST("translate")
  fun translate(@Body req: TransRequest): Call<TransResponse>
}

这里还需要注意的是,根据LibreTranslate API的定义,执行POST请求发送的数据格式应该是Json,所以还需要用@Headers注解在请求的头部信息处指定数据格式。

然后就可以用这个接口动态创建Retrofit对象,再添加Gson解析器工厂类(Gson是Google开发的一种同时能将Json字符串解析为类对象、也能将类对象编码为Json字符串的库),接着调用这个封装的功能方法并加入后台操作队列中,最后实现一个回调类,分别处理成功获得数据的操作(解析response body对应的json以获取翻译结果,并显示在屏幕上)和请求失败的操作(在屏幕上显示错误信息)。代码如下所示:

private fun remoteTrans(text: String, srcLang: String, dstLang: String, https: Boolean) {
        binding.responseText.text = "Please wait..."
        val retro = Retrofit.Builder().apply {
            val proto = if(https) "https" else "http"
            baseUrl("${proto}://$baseIp:$port/")
            addConverterFactory(GsonConverterFactory.create())
        }.build()
        val serv = retro.create(ApiService::class.java)
        serv.translate(TransRequest(text, srcLang, dstLang, "text", "")).enqueue(object : retrofit2.Callback<TransResponse> {
            override fun onResponse(call: Call<TransResponse>, response: Response<TransResponse>) {
                val translatedText = response.body()?.translatedText
                if (translatedText != null) {
                    binding.responseText.text = translatedText
                } else {
                    binding.responseText.text = "Translation failed"
                }
            }

            override fun onFailure(call: Call<TransResponse>, t: Throwable) {
                binding.responseText.text = "Error occurred: ${t.message}"
                t.printStackTrace()
            }
        })
    }

最后就是设计一个简单的UI界面了,布局设计大致思路如下:用户在文本输入框中输入待翻译的文本,再用Spinner下拉选项框选择源语言和目标语言,然后点击翻译按钮进行翻译操作。翻译结果将显示在文本视图中。根部局采用LinearLayout或者RelativeLayout都可以,我个人喜好采用RelativeLayout。那么布局的代码如下:
 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:title="LibreTrans App (Argos)" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/toolbar"
        android:padding="16dp">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/inputLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/inputText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Enter text" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnTranslate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/inputLayout"
            android:layout_marginTop="16dp"
            android:text="Translate" />

        <Spinner
            android:id="@+id/srcLangSpinner"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/btnTranslate"
            android:layout_marginTop="16dp" />

        <Spinner
            android:id="@+id/dstLangSpinner"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/srcLangSpinner"
            android:layout_marginTop="16dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/dstLangSpinner"
            android:layout_marginTop="16dp"
            android:background="@drawable/border_background"
            android:padding="8dp">

            <TextView
                android:id="@+id/responseText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:text="Translation result will be shown here" />
        </LinearLayout>

    </RelativeLayout>

</RelativeLayout>

然后在onCreate中注册控件的事件处理函数,并获取用户在设置界面预先存储在SharedPreferences中的配置信息(包括搭建服务的局域网IP地址、端口号、是否使用HTTPS,此处省略设置界面的代码,具体逻辑可以参照下文给出的完整代码),最后调用翻译接口,主要代码如下:

private val languages = arrayOf("de", "en", "es", "fr", "it", "zh")
private lateinit var sharedPreferences: SharedPreferences
private lateinit var baseIp: String
private lateinit var port: String
private var useHttps: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)

        val spinnerAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, languages)
        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        binding.srcLangSpinner.adapter = spinnerAdapter
        binding.dstLangSpinner.adapter = spinnerAdapter
        binding.srcLangSpinner.setSelection(1)
        binding.dstLangSpinner.setSelection(2)

        binding.btnTranslate.setOnClickListener {
            baseIp = sharedPreferences.getString("api_ip", "127.0.0.1") ?: "127.0.0.1"
            port = sharedPreferences.getString("api_port", "5000") ?: "5000"
            useHttps = sharedPreferences.getBoolean("use_https", false)
            val text = binding.inputText.text.toString()
            val srcLang = binding.srcLangSpinner.selectedItem.toString()
            val dstLang = binding.dstLangSpinner.selectedItem.toString()

            if (text.isNotEmpty()) {
                remoteTrans(text, srcLang, dstLang, useHttps)
            } else {
                Toast.makeText(this, "Please enter text to translate", Toast.LENGTH_SHORT).show()
            }
        }
    }

这样一来,基于LibreTranslate的局域网翻译Android客户端就完成了,下面来看看效果:
 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值