Pythonで画面領域をORCしてdeeplで翻訳するアプリケーションを作ってみた

##はじめに
海外のちょっとしたRPGとかADVをやってるとき、ちょっとこれ何言ってるかわからないなって思うことありますよね。
そんなわけで趣味と実益を兼ねて画面の領域を選択して文字を読み取り翻訳するアプリケーションを作ってみました。
これはその備忘録的なモノです。とりあえず動くものって感じに作ったのでコードが汚いのはまあ、はい。

ぶっちゃけ似たようなものはちょっとググったらあったんですが([OCR2DeepL](https://news.yahoo.co.jp/articles/68eeec61b377bbfd7b5fbacdb641d17bea7f21c7))、これはORCで領域から文字を読み込んでクリップボードに入れるフリーソフトを監視してデスクトップ版deeplに飛ばすっていうなんとも言えないものだったので「作るか～」って感じになりました。

##流れ
1. ホットキーで起動する
2. windowsの「切り取り&スケッチ」(旧: Snipping Tool)みたいに画面領域のスクショを撮る
3. ORCで文字を読み取る
4. deeplで翻訳する

### 1. ホットキー
まず詰まったのがPythonでウィンドウが非アクティブでも動作するホットキー、つまりグローバルキーフックする方法でした。なんかあんまり方法がなさそうなんですよね。(筆者の検索力が弱い可能性もある)
候補としては、

- pyHook
- pyHooked
- wxPython

あたりが使えそうだということがわかりました。
wxPythonはホットキー用というよりGUIツールキットで、機能の一部としてホットキーが提供されているという感じなのですが、今回はSnipping Tool的な画面選択を行うのにGUIを組む必要があったのでこれを採用しました。
といってもwxPython自体初見な上、そもそも普段GUIなんて組まないなの随分手間取りましたが。

### 2. 画面領域選択
画面領域選択は「[PythonでWindowsのスニッピングツールらしきものを実装してみた](https://qiita.com/harupy/items/d4f7978c4d8d1c2a3d01)」とドンピシャな記事があったので参考にしました。
これはPyQt5で書かれてますが、wxPythonに書き直しました。PyQt5でホットキー使えれば楽だったんですが。
大まかな動作としては、

1. ホットキーで半透明なフルスクリーンウィンドウをトップレベル固定で生成
2. ドラッグに合わせて色の違う四角いフレームを表示して選択領域を可視化
3. ウィンドウを消してから選択領域をPillowで切り抜く

って感じです。

### 3. OCR
これに関しては特に何もないですね…
pyocrでtesseract使ってます。

あ、ちょっとした工夫としてまず出てこないだろう|をIに置き換えたり、改行が挟まると文が途切れちゃうので消したりしてます。
場合によっては二値化とかの前処理を通してもいいですね。
こんな風に手を加えられる点ではOCR2DeepL使わずに自分で作ったかいがあったと思います。

### 4. 翻訳
ここがちょっと悩んだところなんですが…
はじめはdeeplのpythonライブラリを当てにしていました。なんか2,3個あるので。
でもアレ、日本語対応してるやつが一つもなかったんですよね…
というわけで悲しみに明け暮れながら他の手段を探すことに。

まず、デスクトップ版deeplには外部インターフェースとか無いです。強いて言えばPyAutoGuiでctrl+c連打したりすれば一応貼り付けたりできます(OCR2DeepLは多分この方式)。
かといってdeeplのAPIは有料です。こんなお遊びでわざわざそれを使うのは…と悩んでいたのですが、よく考えたらブラウザ上なら好きに使えることに気づきました。

というわけで、今回はSeleniumでChromeを操作してdeepl.comを開き、入力欄にsend_keysで書き込んでます。はい。力技です。

ちなみにChromeの起動オプションってヘッドレスモード以外にも色々あって、その中にアプリモードってのがあります。Twitterのweb版をデスクトップでインストール?したときとかと多分同じ状態だと思うんですが、これはそのサイトのみを開いてその他のChrome的な要素を省いたようなウィンドウを開くことができ、大変スマートです。
指定方法は引数に--app=urlオプションを指定します。
今回はこのモードを利用してdeeplを開きました。

## ソースコード
完成したソースコードはこんな感じです。

```py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import wx
from PIL import ImageGrab, Image
import pyocr
import pyocr.builders
import re
from selenium import webdriver
import chromedriver_binary

class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, -1, '', (0, 0), wx.DefaultSize, wx.STAY_ON_TOP)
        self.SetSize(wx.Display().GetGeometry().GetSize())
        self.SetBackgroundColour('#FFFFe0')
        self.SetTransparent(30)
        self.regHotkey()
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
        self.panel = wx.Panel(self, -1)
        self.panel.SetBackgroundColour('Blue')
        self.panel.Hide()
        
        tools = pyocr.get_available_tools()
        if len(tools) == 0:
            exit(1)
        self.tool = tools[0]

        options = webdriver.ChromeOptions()
        options.add_argument('--app=https://www.deepl.com/translator')
        driver = webdriver.Chrome(options=options)
        driver.implicitly_wait(20)
        self.src_textarea = driver.find_element_by_css_selector('.lmt__textarea.lmt__source_textarea.lmt__textarea_base_style')

    def regHotkey(self):
        new_id = wx.NewIdRef(count=1)
        self.RegisterHotKey(new_id, wx.MOD_ALT, wx.WXK_SPACE)
        self.Bind(wx.EVT_HOTKEY, lambda event:self.Show(True), id=new_id)

    def OnMouseLeftDown(self, event):
        # print('MouseDown')
        x1, y1 = x0, y0 = wx.GetMousePosition()
        self.panel.SetSize(x0, y0, 0, 0)
        self.panel.Show()
        while wx.GetMouseState().leftIsDown:
            x1, y1 = wx.GetMousePosition()
            xl, xg = sorted((x0, x1))
            yl, yg = sorted((y0, y1))
            self.panel.SetSize(xl, yl, xg - xl, yg - yl)
            # print(xl, yl, xg - xl, yg - yl)
        self.panel.Hide()
        # print('MouseUp')
        self.Hide()

        im = ImageGrab.grab(bbox=(xl, yl, xg, yg))

        en_text = self.tool.image_to_string(
            im,
            lang="eng",
            builder=pyocr.builders.TextBuilder(tesseract_layout=6)
        )
        en_text = re.sub(r'\s+', ' ', en_text).replace('|', 'I')
        print(en_text)
        self.src_textarea.clear()
        self.src_textarea.send_keys(en_text)


class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame()
        self.SetTopWindow(frame)
        self.SetExitOnFrameDelete(False)
        return True

if __name__ == '__main__':
    app = MyApp(0)
    app.MainLoop()
```
## 感想
今回作ってみて思ったことは、まずPythonはホットキーに向いてないってことですね。どのライブラリでも押したホットキー自体の入力がキャンセルされないので他の動作の邪魔になっちゃいますし、自由度も低いです。
ホットキーを登録できるスクリプト言語はやはりAutoHotkeyが最強ですね。日本語wikiの更新2013年ですけど。

あとwxPythonすごい気持ち悪いです。命名規則が。なんじゃこれ。C#かなんかか？ PEP8はどうした。
できれば二度と使いたくないです。

それはそれとして、まあ結果としては自分で使う分にはそこそこ便利なモノが作れたので良かったですね。読み取り精度は完全にtesseract依存ですけどそれはそれ。

お読みいただきありがとうございました。