DeepL API x PySimpleGUIで翻訳支援アプリを作成

#はじめに
はじめまして。NSシステムズと申します。


今更ながらDeepLの世界一の翻訳精度かつ無料のDeepL API Freeに感動しましたので、
PythonのGUIライブラリで一番簡単なPySimpleGUIを使って翻訳支援デスクトップアプリを作成してみました。PySimpeGUIはとても簡単に使えますので、見た目に強いこだわりがなければ取り敢えず使ってみていただきたいなと思います。

また、精度は間違いなく世界一なのですが利用料金も世界一なのか？という
ところも検証しております（要するにDeepL有料版はお得なのかを検証しています）。

Qiita初投稿ですが、どなたかのお役に立てばと思います。

#アプリの概要
今回開発するアプリは、DeepLAPIFreeを使って日⇄英翻訳を実施するシンプルなアプリです（図1）。
![スクリーンショット 2021-11-29 午後06.43.07 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2260252/2b2bcbef-f2cf-69e5-b5f9-1f3b8a2fd709.png)
DeepLで翻訳するだけならば、オフィシャルのデスクトップ版DeepLアプリを使えば良いという声が聞こえてきそうですがデスクトップ版アプリは１回で翻訳出来る文字数が5000字という制限があります（無料版の場合）。
ただし、DeepL APIFreeを使えば、１回当たりの翻訳文字数制限はありません。500000字/月まで無料で使えます。ハリーポッターの賢者の石を丸ごと翻訳しても約36万字なので、趣味の範囲であれば十分です。

##アプリのレイアウト説明

##アプリのディレクトリ構成
今回開発するアプリのディレクトリ構成を検討します。再利用出来るようにDeepLAPI関連のdeepl.py、GUI関連のgui.py、そしてプログラムのエントリーポイントのmain.pyと３つのファイルに分けて作成します。

```
DeepL_GUIApp
 ├── main.py
 ├── deepl.py
 └── gui.py
```

##開発に必要なパッケージ
・ PySimpleGUI

##GUIレイアウトGUI.pyの作成
 PySimpleGUIを使ってGUIレイアウトを作成してきます。図2で示すように、レイアウトに配置するウィジェットは以下の4種類で合計8ヶです。

・ Text ... ①②⑥⑦
・ Multiline ... ③④
・ Button ... ⑤
・ ProgressBar ... ⑥⑦
![NOTE用Pythonで作るGUIアプリ入門.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2260252/ab6282c3-c805-2e6e-1bbd-76946d71be92.png)

コードは以下の通りとなります。コード内では2つの関数を宣言しています。
1つ目は`main_window()`関数です。GUIウィンドウを関数化しています。
2つ目の`get_usage()`関数ではDeepL使用率を取得してウィンドウ内のプログレスバーを更新します。

```python:gui.py 
import PySimpleGUI as sg # PySimpleGUIをインポート
import deepl # 自作のdeepl.pyをインポート

sg.theme('Reddit')

# メインウィンドウ
def main_window():
    layout = [
        [sg.Text('原文(日、英は自動判定します)', size=(37, 2), justification='center'),   # ...①
         sg.Text('DeepL翻訳（原文⇨DeepL翻訳））', size=(37, 2), justification='center')], #  ...② 
        [sg.Multiline('', key='-original_text-', size=(35, 10)),   # ...③
         sg.Multiline('', key='-translated_text-', size=(35, 10))],  # ...④ 
        [sg.Button('DeepL翻訳', size=(10, 1), key='-run_deepl-', pad=(190, 20))],  # ...⑤
        [sg.Text('DeepL使用量', size=(74, 1), justification='center')],  # ...⑥
        [sg.Text('',size=(74, 1), key='-Char_cnt-', justification='center')],  # ...⑦ 
        [sg.ProgressBar(100, orientation='h', key='-usage_bar-', visible=True, size=(35, 20),
                        pad=(70, 10))] # ....⑧
    ]

    return sg.Window("翻訳支援アプリ", layout, finalize=True)

# DeepL使用率を取得し、Windowをアップデート
def get_usage(window):
    count_result = deepl.char_cnt()
    if count_result is None:
        sg.popup('APIキーを確認して下さい')
    else:
        count_used = count_result['character_count']
        count_limit = count_result['character_limit']
        bar_used = count_used / count_limit * 100
        output_txt = str(count_used) + ' / ' + str(count_limit) + '文字'
        window['-usage_bar-'].update(bar_used)
        window['-Char_cnt-'].update(output_txt)

```

### 各種モジュール＆自作関数のimport
1行目では、PySimpleGUIをインポートしています。
2行目では、自作のdeepl.pyをインポートしています。

`sg.theme(‘Reddit’)`は、PySimpleGUIのテーマ’Reddit’を指定しています。
公式ページにさまざまな[テーマ](https://pysimplegui.readthedocs.io/en/latest/#themes-automatic-coloring-of-your-windows)が紹介されていますので、ご興味ありましたら参照ください。

### main_window()関数について
この関数内でWindowのレイアウトを設定して、戻り値として設定したlayoutを返します。
以下の説明に加えて図2とgui.pyのコードも合わせて参照下さい。図中のウィジェットの番号とコード内の番号を一致させております。

①②：テキストウィジェットで下のMultilineウィジェットの説明をしています。
③④：Multilineウィジェットです。サイズはどちらも35字、10行で設定しています。
　　　左側に翻訳したい原文を右側に翻訳結果を反映させます。
　　　キーは、それぞれ'-original_text-'と'-translated_text-'として設定しています。
⑤：[ボタンウィジェット](https://note.com/nssystems/n/n41dfc85a707f)でこちらのボタンを押すとDeepL翻訳が実行されるように
　　　イベントを設定します。キーは、'-run_deepl-'です。
⑥：テキストウィジェットでDeepL使用量として⑦、⑧の説明です。
⑦：テキストウィジェットでDeepLの現在の使用量をテキストで表示します。
　　キーは、'-Char_cnt-'として設定しています。
⑧：[プログレスバーウィジェット](https://note.com/nssystems/n/n56e919b593ea)でDeepLの現在の使用量を％バーで表示します。
　　キーは、'-usage_bar-'として設定しています。

それぞれのウィジェットのもう少し詳細な説明は、[こちら](https://note.com/nssystems/m/m3a0777dc8787)で説明していますのでご興味のありましたら参照下さい。


### get_usage(window)関数について
この関数には引数としてwindowを渡します。DeepL APIで使用率を取得してwindowをアップデートします（図2参照）。
関数内の1行目では、自作のdeepl.pyで宣言している`char_cnt()`を使用して辞書型変数count_resultに格納しています。変数のキーと値は以下の通りです。

+ character_count：現在のDeepL使用文字数
+ character_limit：DeepL文字数リミット（DeepLAPI Freeであれば500000字です）

`if count_result is None:`でDeepLAPIから値を取得出来なかった場合のメッセージをポップアップで表示させています。

値を取得出来た場合は、現在のDeepL使用文字数を`count_usege`、DeepL文字数リミットを`count_limit`としてcount_resultから取り出します。
そしてDeepLの使用率（％）を`bar_used`、使用率の具体的な数値を`output_txt`に入れて、それぞれの値でウィンドウの各ウィジェットを更新しています。


##DeepL API関連処理のdeepl.pyの作成
deepl.pyでは以下の３つの関数を定義しています。

+ `translate(text, s_lang, t_lang)`：DeepL翻訳を実行し、翻訳結果を返す
+ `char_cnt()`：DeepL APIの使用量を返す
+ `lang_set(text)`：引数textが日本語かどうかを判定し翻訳先の言語を返す

deepl.pyのコードは以下の通りです。

```python:deepl.py
import json
from urllib.parse import urlencode
from urllib.request import Request
from urllib.request import urlopen
from urllib.error import HTTPError
import re

config = {'Auth_key': 'ご自身のAuth＿Keyを貼り付けて下さい',
          'Translate_ep': 'https://api-free.deepl.com/v2/translate',
          'Usage_ep': 'https://api-free.deepl.com/v2/usage'}


def translate(text, t_lang):
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded; utf-8'
    }

    params = {
        'auth_key': config['Auth_key'],
        'text': text,        
        'target_lang': t_lang
    }

    req = Request(
        config['Translate_ep'],
        method='POST',
        data=urlencode(params).encode('utf-8'),
        headers=headers
    )

    try:
        with urlopen(req) as res:
            trans_result = json.loads(res.read().decode('utf-8'))            
            return trans_result['translations'][0]['text']            
    except HTTPError as e:
        print(e)


def char_cnt():
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded; utf-8'
    }

    params = {
        'auth_key': config['Auth_key'],
    }

    req = Request(
        config['Usage_ep'],
        method='POST',
        data=urlencode(params).encode('utf-8'),
        headers=headers
    )
    try:
        with urlopen(req) as res:
            cnt_result = json.loads(res.read().decode('utf-8'))
            return cnt_result

    except HTTPError as e:
        print(e)

def lang_set(text):
    # default setting
    t_lang = "JA"
    if re.search(r'[ぁ-んァ-ン]', text):
        t_lang = "EN"

    return t_lang

```
### 各種モジュール＆自作関数のimport
DeepLAPIの戻り値はJSON形式となりますので、jsonをインポートします。
また、WebAPIを使用しますのでurllibパッケージから今回必要な`urlencode`、`Request`、`urlopen`、`HTTPError`モジュールをインポートします。
最後に正規表現を使いますのでreモジュールをインポートします。

また、変数configにDeepLAPIの各種環境変数を指定します。
Auth _keyのところには、ご自身のDeepLAPI FreeのAPIキーを貼り付けて下さい。
Translate_epとUsage_epはそれぞれ翻訳する場合と使用率を確認する場合のエンドポイントになります。

### translate(text, t_lang)関数について

この関数は仮引数として翻訳したい文字列`text`、翻訳先の言語コード`t_lang`を取ります。
翻訳元の言語コードは、deepLが自動で判定してくれますので指定しなくても大丈夫です。
今回は、日（JA）⇄英（EN）翻訳だけとしますが他の言語も言語コードを指定すれば翻訳出来ます。

header、params、reqについては、[DeepLAPI技術資料](https://www.deepl.com/docs-api/)に記載がありますので
説明は公式ページを参照下さい。

`with urlopen(req) as res:`でJSON形式のレスポンスを受け取ります。
以下は公式ページに記載のDeepLAPIからのレスポンス例です。

`{
	"translations": [{
		"detected_source_language":"EN",
		"text":"Hallo, Welt!"
	}]
}`

このレスポンスをjson形式からPythonで使いやすい辞書型に`json.loads`で変換し`trans_result`へ格納します。

戻り値は、翻訳結果だけを返したいので、`trans_result['translations'][0]['text']`として返します。

### char_cnt()関数について

この関数は、DeepL APIの使用量を取得します。
header、params、reqについては、先ほどと同様に[DeepLAPI技術資料](https://www.deepl.com/docs-api/)に記載がありますので
説明は割愛しますが、`translate()`関数との違いは、パラメータが少ない事（翻訳しないので当然ですが）、
リクエスト先エンドポイントが`Usage＿ep`である事です。
変数`cnt_result`に以下のようなjson形式のレスポンスを受け取ります。こちらも辞書型に変換しています。

`{
	"character_count": 180118,
	"character_limit": 500000
}`

### lang_set(text)関数について

この関数は仮引数として翻訳したい文字列`text`を取ります。
翻訳先の言語を入れる変数として`t_lang`を宣言して、デフォルトで日本語コード’JA'を入れます。
if文で正規表現を使って`text`にひらがな、もしくはカタカタが含まれているかをチェックして、含まれている場合は日本語であると判断して`t_lang`を`EN`に書き換えます。

戻り値は、JAもしくはENの言語コードです。

## エントリーポイント main.pyの作成

main.pyにアプリの一連の処理をコーディングしていきます。
アプリの流れは以下の通りです。

1. GUIウィンドウを呼び出す
2. DeepL使用量を取得してGUIウィンドウに反映
3. GUIイベント処理(イベントを待ち続ける)
    + DeepL実行ボタンが押された場合の処理
    + GUIウィンドウの閉じるボタンが押された際の処理

main.pyのコードは以下の通りです。

```python:main.py
import PySimpleGUI as sg
import gui
import deepl


def main():    
    # 最初に表示するウィンドウを指定する。
    window = gui.main_window()
    # deepLの使用量を取得し、ウィンドウに表示させる。
    gui.get_usage(window)

    while True:
        event, values = window.read()
        if event == '-run_deepl-':
            message = values['-original_text-']

            # 翻訳先の言語を設定
            lng = deepl.lang_set(message)
            # DeepL翻訳実行
            trans_result = deepl.translate(message, t_lang=lng)
            # ウィンドウに翻訳結果を反映させる
            window['-translated_text-'].print(trans_result)

            # deepLの使用量を取得し、ウィンドウに表示させる。
            gui.get_usage(window)
       
        if event == sg.WIN_CLOSED or event == "Exit":
            break

    window.close()


if __name__ == '__main__':
    main()
```

### 各種モジュール＆自作関数のimport
GUIアプリですので、PySimpleGUIをインポートします。
また、先ほど作成したgui.pyとdeepl.pyもインポートします。

### main()関数について

#### プログラムの初期設定（GUIウィンドウの呼び出し）
まずは、GUIウィンドウを呼び出します。
`window = gui.main_window()`

そして、deepLの現在の使用率を取得してウィンドウに反映します。
`window = gui.main_window()`

`while True:`以下は、GUIのイベントループになります。

#### イベント：DeepL翻訳処理
DeepL翻訳ボタンには、`key = '-run_deepl-'`を設定していました。
ボタンが押された際の処理をif文内にコーディングします。
`if event == '-run_deepl-':`

`message = values['-original_text-']`として、GUIウィンドウ内の左側の入力エリアから翻訳したい文字列を変数`message`に取得します。

この`message`を先ほど`deepl.py`で定義した`lang_set()`関数に渡して翻訳先の言語コードを変数`lang`に取得します。
`lng = deepl.lang_set(message)`

翻訳したい文字列`message`と翻訳先の言語コード`lang`をdeepl翻訳の実行関数である`translate()`に渡して、翻訳を実行して実行結果を変数`trans_result`に取得します。
`trans_result = deepl.translate(message, t_lang=lng)`

翻訳結果をウィンドウの右側のエリアに反映させます。
`window['-translated_text-'].print(trans_result)`

翻訳語は、DeepL使用率を更新するため再度`get_usage(window)`を呼び出してウィンドウを更新します。

以上で一連のDeepL翻訳処理は完了になります。

#### イベント：プログラムの終了処理
`if event == sg.WIN_CLOSED or event == "Exit":
            break`

こちらは、閉じるボタンが押された場合にプログラムを終了する処理です。




# DeepL API Freeの利用登録
DeepL API Freeは無料ですが、登録の際にクレジットカードが必要です。これは、フリープランの乱用を防ぐためのようです。
500000字/月を超えた場合でも自動的に課金される事はありません。

利用登録方法については、[こちら](https://note.com/nssystems/n/ndfb625022183 )に画像多めでにまとめておりますので参照下さい。

# DeepL APIはコストも世界一か？ 

DeepLの翻訳精度は、小生知りうる限りでは現時点で一番だと思いますが、その分やはりコストも高いのかが気になります。
DeepL API Freeは500000字までは無料ですが、ビジネス利用だと全く足りません。
ビジネス用途に有料版のDeepL API Proも提供されています

こちらの料金は以下の通りです。API ProでFree版と同じ50万文字を使用すると税込で1880円/月が必要となります。
・ 100万文字あたり2500円（税別）
・ 基本料金も630円/月 

有料版のメリットの一つは用語集なのですが、日本語は現時点で対応しておりませんので大きなメリットを感受できません。

これ以外のメリットは、50万文字以上でも翻訳出来ることととデータセキュリティーになります。企業などで使用するのであれば、データセキュリティーは非常に重要ですが、個人で趣味等で使うのであればFreeで十分かと思います。

## 他サービスとの比較

国立研究開発法人情報通信研究機構が開発した、企業向けクラウド翻訳サービスMirai Transerと料金を比べてみます。
実際にどちらも使ってみましたが、精度はどちらもかなり高く、感動してしまうレベルです。
それでも、DeepLが一歩リードしている気がします。

Mirai Transerは企業向けなので、それなりの金額となっています。国内最大規模の産業翻訳サービス企業である翻訳センターのページに料金表が掲載されていました。英語→日本語の場合、基本料金が10000円/月でさらに１ワード１円です。英単語は、１ワード平均して4.7文字くらいですのでざっくり5文字で1円とすれば、1文字当たり0.2円です。

一方でDeepL API Proは1文字当たり0.0025円で基本料金は650円/月だけです。
文字数に応じて、どれだけの差が出るかを比較してみました（下表参照)。

![DeepLと未来トランサーの比較.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2260252/c66b3358-7f87-5f64-a304-7db56076ca86.png)

MiraiTranserは企業向けという事でプロ向けの金額設定となっていますが決して高いわけではないようです。
一般社団法人日本翻訳連盟が公表している翻訳料金は１ワード28円程度ですのでMiraiTranserの料金は1/28分の一です。（もちろん機械翻訳ですので、プロの翻訳家には敵いませんので単純な料金比較はフェアではありません）

DeepL API Proはさらに安いので、DeepLAPIProのコストが圧倒的にお得である事がわかりました。


以上の比較か機械翻訳同士の比較であればらDeepLのコストが圧倒的に安いということがわかりました。

精度、コスト共に圧倒的なDeepLAPIがこれまで以上に日本で広まればDeepL社も日本向けのサービスを拡充
（用語登録機能など）してもらえると思いますので、みんなでたくさん使いましょう。


# 最後に

本当に今更な感じですが、私のようにDeepL翻訳を最近まで使っていない人もいるかもしれませんのでそのような方は是非とも使ってみて下さい。素直に凄いな。ここまで来たかと感動されると思います。

今回作成したアプリよりももう少し複雑なアプリ作成について↓で説明しております。
・[Pythonで作るGUI付き翻訳支援アプリの作成](https://note.com/nssystems/n/n6361475f9b9a?magazine_key=m3a0777dc8787)

こちらのアプリでは、原文→翻訳→逆翻訳 が出来るようになっております。逆翻訳をする事で翻訳結果が正しく翻訳されているのかを確かめる事が出来ます。特に、日本語は主語を抜いてしまう事が多いので逆翻訳結果を見返すと主語が足りないために意味が異なってしまうことなどを防ぐ事が出来ます。


DeepLAPIFreeは無料で使えます。活用の用途はたくさんあると思いますので、ご自身のアプリに組み込んでみてはいかがでしょうか？