逆翻訳して遊びたい！でも翻訳APIは有料なこと多いし・・・え？Pythonでローカルで動く翻訳ライブラリが？

## いつもの読み飛ばしポイント

どうも限界派遣SESのnikawamikanです。
アドカレもあと5日で終わりますね。25記事書くの頑張ります。
ちなみに本日も遅れています。

本日はクソアプリを作っていこうと思います。

https://github.com/nikawamikan/re_translator

## 逆翻訳ってクソおもろい

逆翻訳といえば何を思い浮かべますか？
私はバイヤー高橋さんです。

https://www.youtube.com/watch?v=S6TkKDbVghw&list=PLbNHOc8TI4QbPkurEjphiPcqPpmTtCTQN&index=1

くっそ面白いですよねｗ

現代ではGoogle翻訳やDeepLなど真面目に使えるWeb翻訳などが充実していますが、これらをAPIとして使おうとすると有料なことが多いです。
仕方ないことですね。ディープラーニングのコストなどを考えると、Web翻訳を無料で使えるのはありがたいことです。

しかし、Pythonにはローカルで使える翻訳ライブラリがあります。

今回は`argostranslate`を使って逆翻訳APIを作っていきます。

## 環境構築

今回は以下の環境で逆翻訳APIを作っていきます。

- Python 3.12

### ライブラリのインストール

まずは以下のライブラリをインストールします。

| ライブラリ     | 用途                                 |
| -------------- | ------------------------------------ |
| argostranslate | 翻訳ライブラリ                       |
| uvicorn        | サーバー                             |
| fastapi        | APIフレームワーク                    |


```bash
pip install argostranslate uvicorn fastapi
```

次に`argostranslate`のモデルをダウンロードしておきます。

```bash
argospm install translate
```

これを実行すると`${HOME}/.local/cache/argos-translate`にモデルがダウンロードされます。

## 逆翻訳APIの作成

それでは逆翻訳APIを作成していきます。

### 翻訳関数

まずは翻訳関数を作成します。

```python
from argostranslate import translate
from functools import lru_cache

@lru_cache(2 ** 10) # キャッシュを使って同じ組み合わせの翻訳を高速化
def translate_text(text: str, source_lang: str, target_lang: str) -> str:
    translated_text = translate.translate(
        text, # 翻訳したいテキスト
        source_lang, # 翻訳前の言語
        target_lang # 翻訳後の言語
    )
    return translated_text
```

この翻訳ですが、CPUで動作させる場合結構時間がかかります。  
私のサーバーマシンはGPUを搭載していないので、残念ですがCPUで動かします。

なので、少しでも高速化するために`functools.lru_cache`を使ってキャッシュを使います。
これは引数が同じ場合、結果をキャッシュしたものを返すことの出来るデコレータです。

次に逆翻訳関数を作成します。

```python
def re_translate_text(text: str, source_lang: str, via_langs: list[str]) -> str:

    # 元の言語から中間言語へ翻訳
    translated_text = translate_text(
        text,
        source_lang,
        via_langs[0]
    )

    # 中間言語から中間言語へ翻訳
    for i in range(len(via_langs) - 1):
        translated_text = translate_text(
            translated_text,
            via_langs[i],
            via_langs[i + 1]
        )

    # 中間言語から元の言語へ翻訳
    translated_text = translate_text(
        translated_text,
        via_langs[-1],
        source_lang
    )
    return translated_text
```

この関数は、元の言語から中間言語へ翻訳し、中間言語から中間言語へ翻訳し、中間言語から元の言語へ翻訳する関数です。
via_langsには中間言語のリストを渡します。
中間言語が1個の場合は、元の言語から中間言語へ翻訳し、中間言語から元の言語へ翻訳するだけです。
2個以上の場合は、中間言語から中間言語へ翻訳を行います。

### FastAPIの作成

次にエンドポイントを定義します。

```python
from fastapi.responses import PlainTextResponse

@app.get("/re_translate")
def re_translate(text: str, source_lang: str, via_langs: str):
    """再翻訳して人間が理解できないテキストに翻訳します。
    """
    return PlainTextResponse(
        re_translate_text(
            text,
            source_lang,
            via_langs.split(",")))
```

`@app.get`デコレータを使って、GETリクエストを受け取るエンドポイントを作成します。  
`/re_translate`エンドポイントに`TranslateModel`を受け取り、`re_translate_text`関数を呼び出して結果を返します。

この手軽さがFastAPIのいいところですね。

### サーバーの起動

サーバーを起動して、APIを叩いてみます。

```bash
uvicorn main:app --reload
```

`--reload`オプションをつけると、ファイルの変更を検知してサーバーを再起動してくれます。

これで実行し`http://127.0.0.1:8000/docs`にアクセスすると、SwaggerUIが表示されます。

ここではエンドポイントを試すことができるので、試してみましょう。

textに`今日のご飯何にする？おでん？みかん？`と入力し、source_langに`ja`、via_langsに`en,ru`を入力して`Execute`をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/0a6bab68-df49-598b-32d4-9d21c8d612ca.png)

すると、`ペットと一緒に何をしますか?`という結果が返ってきました。

いいですねｗ

## Discord Botに組み込む

汎用的に利用することを考えるとAPIのほうが便利ですが、人に見せたりするにはDiscord Botにすると見せやすくて面白いですよね。

### Botの作成

まず追加のライブラリをインストールします。

| ライブラリ | 用途                  |
| ---------- | --------------------- |
| py-cord    | Discord Botライブラリ |
| aiocache   | キャッシュライブラリ  |

```bash
pip install py-cord aiocache
```

次にBotを作成します。

```python: main.py
import discord
from discord import option
import aiohttp
from aiocache import cached

intents = discord.Intents.default()

bot = discord.Bot(intents=intents)

# 翻訳関数
@cached(600) # キャッシュを使ってBOT側で再問い合わせを減らす
async def translate_text(text: str, source_lang: str, via_langs: str) -> str:
    encoded_text = quote(text)
    encoded_source_lang = quote(source_lang)
    encoded_via_langs = quote(via_langs)

    async with aiohttp.ClientSession() as session:
        async with session.get(
            f"http://127.0.0.1:8000/re_translate?text={encoded_text}&source_lang={
                encoded_source_lang}&via_langs={encoded_via_langs}"
        ) as response:
            translated_text = await response.text()
    return translated_text

# 逆翻訳コマンド
@bot.slash_command(name="re_translate", description="逆翻訳します")
@option("text", description="翻訳するテキスト")
@option("source_lang", description="翻訳前の言語")
@option("via_langs", description="翻訳する言語 ex) en,ja")
async def re_translate(
    ctx: discord.ApplicationContext,
    text: str,
    source_lang: str,
    via_langs: str,
):  # Takes one integer parameter
    await ctx.defer() # 返信まで時間がかかるので、待機状態にする

    try:
        translated_text = await translate_text(text, source_lang, via_langs)
        # followupで待機状態のメッセージを消して、結果を返す
        await ctx.interaction.followup.send(translated_text)
    except Exception as e:
        await ctx.interaction.followup.send("エラーが発生しました")
        raise e


bot.run("TOKEN")
```

`TOKEN`にはBOTのトークンを入力してください。
できれば、`.env`ファイルなどを使って環境変数にしたほうがセキュリティ的にはいいです。

軽く説明すると、
翻訳関数を定義し、翻訳関数にはAPIの呼び出しキャッシュを取得するために`cached`アノテーションを使っています。`600`秒間キャッシュを保持することで、再問い合わせのコストを下げます。

`@bot.slash_command`デコレータを使って、`/re_translate`コマンドを作成しています。
`@option`デコレータを使って、コマンドのオプションを設定しています。
この`@option`デコレーターはなくてもいいですが、使うことで説明などを設定することができます。

### Botの起動

botを起動は普通に`main.py`を実行するだけです。

```bash
python main.py
```

これでBotが起動します。

DiscordにBotを追加して、`/re_translate`コマンドを実行してみましょう。

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/8a6941b7-0067-ea70-7b1c-1b9fefaa15b1.png)

![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/040f5b03-f1e7-549a-4b74-d2a4f30d9ee7.png)


こんな感じで逆翻訳ができました。

なんかインパクトに欠けますね。


### 画像に変換する

インパクトの強い画像を生成するAPIといえば`5000choyen-api`というものがあります。
これを使って、逆翻訳したテキストを画像に変換してみましょう。

https://github.com/CyberRex0/5000choyen-api?tab=readme-ov-file

まずはAPIを使って画像を生成する関数を作成します。

```py
from io import BytesIO
import discord
from discord import option
import aiohttp
from urllib.parse import quote
from aiocache import cached

@cached(600)
async def text_to_image(text: str):
    escaped_text = escape(text)
    async with aiohttp.ClientSession() as session:
        async with session.get(
            f"https://gsapi.cbrx.io/image?top={escaped_text}&single=true"
        ) as response:
            image = await response.read()

            return image
```

ここで返却される画像は`bytes`オブジェクトなので、これを`discord.File`に変換して送信します。

```py
@bot.slash_command(name="re_translate_image", description="逆翻訳して画像にします")
@option("text", description="翻訳するテキスト")
@option("source_lang", description="翻訳前の言語")
@option("via_langs", description="翻訳する言語 ex) en,ja")
async def re_translate_image(
    ctx: discord.ApplicationContext,
    text: str,
    source_lang: str,
    via_langs: str,
):  # Takes one integer parameter
    await ctx.defer()

    try:
        translated_text = await translate_text(text, source_lang, via_langs)
        image = await text_to_image(translated_text)
        byteio = BytesIO(image)
        byteio.seek(0)
        await ctx.interaction.followup.send(file=discord.File(byteio, "translated_image.png"))
    except Exception as e:
        await ctx.interaction.followup.send("エラーが発生しました")
        raise e
```

これで`/re_translate_image`コマンドを使って、逆翻訳したテキストを画像に変換することができます。

`ByteIO`を使って、`discord.File`に変換しています。


実行すると以下のような画像が生成されます。

![image-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/95e1a59a-f947-7759-102f-9a55768c48bf.png)

## まとめ

今回は`argostranslate`を使って逆翻訳APIを作成しました。  
また、この翻訳精度ですが、Google翻訳やDeepLに比べると精度は低いですが、逆翻訳として使う分には逆に面白い気がします。

クソアプリ作成の参考にしてみてください。

それでは。