OCR結果をDeepLに投げるアプリを作ってみた【Python/JS】

# きっかけ
何て書いてあるんだ...?
![sketch-1639422869518 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240141/923712d1-8992-d738-aa9d-b4ef9ab6f1d0.png)
それは専門外な内容の英語プレゼンを聴講したある日のこと．

普段であればレジェメのPDFが配布され，テキストをコピーしてDeepLに投げることで内容を把握することができるのですが，この日はTeamsの映像でしか見ることができなかったので，専門外な内容だったことも相まって内容を理解するのに苦労しました．

「OCRしてDeepLに投げることができればいいのに...」

ということでOCR機能付きDeepLデスクトップアプリケーションを作りました．

# Summary of technical topics
- OS: Windows10
- GUI: Eel(Python3.7)
- OCR: Tesseract
- Focus the window: Chrome extension (`chrome.windows.update()`)

# 先行事例
作成後に調べたらより多機能なものが色々ありました．

- [Screen Translator](https://github.com/OneMoreGres/ScreenTranslator)    
DeepL以外の翻訳エンジンも利用でき，非常に多機能
- [PCOT](http://www.gc-net.jp/s_54/)  
日本人の方が作成されています

# 作ったもの
Windows10用OCR機能付きDeepLデスクトップアプリケーション

https://github.com/T3aHat/DeepLopenerOCR

![exe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240141/a22662c3-425a-5916-3dbc-0aeb6453a6a4.png)

https://twitter.com/T3ahat/status/1470483271511601156

# できること
- 基本的な挙動は[本家](https://www.deepl.com/ja/app/)を参考にした
    - `Ctrl+C`×2で選択したテキストをDeepLで翻訳
    - 任意のキーボードショートカット設定
    - 自動起動.bat作成機能付き
- OCR
    - OCRソフトである[Tesseract](https://github.com/UB-Mannheim/tesseract)による解析結果をDeepLに投げる

# ペルソナ
- テキスト化されていない英語プレゼン(動画)の聴講者
- 英語ゲームプレイヤー(これは[PCOT](http://www.gc-net.jp/s_54/)の方が使いやすいと思う)

# 技術的な話

## GUI(Eel)
PythonでGUIアプリケーションを作成できるライブラリは様々存在する．

- Tkinter
- wxPython
- PySimpleGUI
- etc.

これらはそれぞれのライブラリ専用の記法で各種ウィジェットを配置していく形になっている．

ウィンドウの透明化など複雑な実装ができる点は魅力だが，ウィジェットの位置を調整するのが(CSSと比べ)大変だと実装して感じたので，HTML/CSSを利用してGUIを作成できないかと探していたところEelにたどり着いた．

https://github.com/ChrisKnott/Eel

EelはHTML/CSSを使ってGUIを作成できるだけでなく，Python⇔JS間で連携ができるため，Python側でキーボード入力監視やOCRといった作業を行った結果をJSに投げてウィンドウに表示するといったことが可能になる．

詳細は [[Python] EelをつかってHTML/CSS/JavaScriptでGUIを構築](https://qiita.com/inoory/items/f431c581332c8d500a3b) などの解説記事を参考にされたい．

## フォルダ構造
以下紹介する各ファイル類は次のようなフォルダ構造からなる．

```
root
└── DeepLopenerOCR.pyw
└── assets
   └── eel.js
   └── main.html
   └── main.js
   └── main.css
```

```python:DeepLopenerOCR.pyw
import eel
eel.init(assets)
eel.start("main.html")
```

とすることで，`assets`フォルダ内の`main.html`をウィンドウに表示できるようになる．

ちなみに`*.py`でなく`*.pyw`とすると，Pythonのコンソールなしで実行できるようになる．
GUIアプリケーションなのでこれは非表示にしたい．


## `Ctrl+C`監視
`send_clipboard`は0.5秒以内に`Ctrl+C`が2連続で押された場合にクリップボードのテキストを翻訳する関数．また，クリップボードのテキストが前に翻訳したときと同じ場合は翻訳しない設定もしている．

```python:DeepLopenerOCR.pyw
import time
import keyboard
import pyperclip

def send_clipboard(target_lang, api_key):
    global now, beforekeycc, beforetxt
    print("key detect")
    if(not keyboard.is_pressed("ctrl+C") and not beforekeycc):
        pass
    elif(time.time()-now < 0.5 and pyperclip.paste() != "" and pyperclip.paste() != beforekeycc:
        # ここで翻訳する
        beforetxt = pyperclip.paste()
    now = time.time()
    beforekeycc = keyboard.is_pressed("ctrl+C")

if __name__ == '__main__':
    # 諸々省略
    keyboard.add_hotkey('ctrl+C', send_clipboard,
                        args=[target_lang, api_key])
```

- `keyboard.add_hotkey('ctrl+C', send_clipboard,args=[target_lang, api_key])`により`ctrl+C`を入力した際に`send_clipboard`関数が実行されるようになる
    - プログラム実行中は常に監視される
    - ほかのホットキーを追加したい場合も同様に指定できる
    
- `keyboard.is_pressed("ctrl+C")`で入力が`ctrl+C`であるか`True/False`

- `pyperclip.paste()`でクリップボードのテキストを取得


## スクリーンキャプチャ監視 & OCR
スクリーンキャプチャした際にOCRを実行する．
先述のScreen TranslatorはスクリーンキャプチャするGUIを作成しているが，自分はそこまでの労力を使いたくなかったので，クリップボードが画像に変化したことをスクリーンキャプチャされたとみなす方針にした．

したがって，`Win+Shift+S`以外にもChromeで`画像をコピー`を押した場合にも発火するようになっている．

<details><summary>ちなみに...</summary><div>
キャプチャGUIを作成するならば，画像取得は`Pillow`の`ImageGrab`，選択範囲を透明/選択範囲外を半透明のグレーにするといった実装をするには，Tkinterの`-transparentcolor`を使えばいけそうに感じた([参考](https://qiita.com/teahat/items/050b572aad0d1686370b))．
</div></details>

なお，クリップボードの内容が変化した際にイベントを発火させる方法が分からなかったため，1秒ごとにクリップボードの内容を取得して
「1秒前から変化している ∧ 画像データになっている」
時にOCRするような実装とした．

```python:DeepLopenerOCR.pyw
from PIL import ImageGrab, Image
import getpass
import pyocr

def ocr(im, target, apikey):
    pyocr.tesseract.TESSERACT_CMD = r'C:\\Program Files\\Tesseract-OCR\\tesseract.exe'
    engines = pyocr.get_available_tools()
    if len(engines) == 0:
        print("Tesseract is not found")
    else:
        engine = engines[0]
        txt = engine.image_to_string(im, lang="eng")
        if(len(txt.replace("\n", "").replace(" ", "")) > 0):
            eel.show("main.html")
            eel.sleep(1)  # 読み込まれるまで待機(雑)
            eel.js_translate(txt, target, apikey)


def capture():
    global target_lang, api_key
    image = ImageGrab.grabclipboard()
    while True:
        im = ImageGrab.grabclipboard()
        if isinstance(im, Image.Image) and im != image:
            ocr(im, target_lang, api_key)
        else:
            pass
        image = ImageGrab.grabclipboard()
        eel.sleep(1)


if __name__ == '__main__':
    # 諸々省略
    spawn = eel.spawn(capture)
```

```javascript:main.js
    eel.expose(js_translate);
    function js_translate(clipboard, target_lang, api_key) {
      // 諸々省略
      api_translate(clipboard, target_lang, api_key);
    }
```

```html:main.html
<html>
    <head>
        <meta charset="UTF-8">
        <title>DeepLopenerOCR</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <!--諸々省略-->
        <script type="text/javascript" src="eel.js"></script>
        <script type="text/javascript" src="main.js">
        </script>
    </body>
</html>
```

### capture()

- `hoge = eel.spawn(capture)`とすることで簡単にマルチスレッド化できる．停止したい場合は`hoge.kill()`で可能．便利．
- `ImageGrab.grabclipboard()`でクリップボードのオブジェクトを読み込むことができる
- `isinstance()`でオブジェクトのデータ型の判定できる(=画像データかどうか) ([参考](https://www.javadrive.jp/python/function/index8.html))


### ocr()
- [tesseract](https://github.com/UB-Mannheim/tesseract)を予めインストールしておく
- `pyocr.tesseract.TESSERACT_CMD = r'C:\\Program Files\\Tesseract-OCR\\tesseract.exe`でインストールした`tesseract.exe`のパスを明示的に定義
- `txt = engine.image_to_string(im, lang="eng")`でOCR解析を行う
    - `lang`を指定することで解析言語を変更可能(日本語も対応可能)
- `eel.show("main.html")`で`main.html`を新しいウィンドウで開く
- `eel.sleep(1)`
- `eel.js_translate(txt, target, apikey)`
    - `js_translate()`は`main.js`内で定義している関数
    - しかし，ウィンドウを開いた直後は実行できないので`eel.sleep(1)`で1秒待っている(雑)


## Focus the window
翻訳実行時はウィンドウが最前面に出てきてほしい．
以前は`win32gui.SetForegroundWindow`により実現できていたようだが，手元の環境では期待した挙動にならなかった([参考](https://teratail.com/questions/187348))．

そこで，Pythonで制御することをあきらめてChrome拡張機能に着目した．
EelはデフォルトではChromeをapp-modeで起動するので拡張機能を利用できる．

https://github.com/T3aHat/DeepLopenerOCR/tree/main/extension

キーとなる部分は以下のみ([参考](https://developer.chrome.com/docs/extensions/reference/windows/#method-update))．

```javascript:background.js
    chrome.windows.update(
      sender.tab.windowId,
      { focused: true },
      function (tab) {}
    );
```

## exe化
- Pyinstallerで可能
- 詳細は[EelのGitHub](https://github.com/ChrisKnott/Eel#building-distributable-binary-with-pyinstaller)を参照

```
python -m eel .\DeepLopenerOCR.pyw assets --icon assets/favicon.ico
```

<details><summary>`--oneflie`と`--noconsole`の共存について</summary><div>
ひとつのexeファイルにまとめる`--onefile`オプションとコンソールを表示させない`.*pyw`もしくは`--noconsole`オプションの両方を選択した状態だとTesseractによる翻訳がなぜかうまくいかなかった(片方であれば問題ない)．
[似たようなことが言及されている記事](https://teratail.com/questions/254226)
</div></details>

# Future Works
- Tesseract以外のOCR(Google Vision等)を選択できるように実装
- OCR言語変更機能
- 任意の範囲の自動スクリーンキャプチャ機能
    - PCOTは実装済み(たぶん)
        - ゲームのテキストウィンドウを設定しておけば洋ゲーが捗りそう

# まとめ
今回はじめてEelを触りましたが，HTML/CSSでウィジェットを配置できるのは楽で良かったです．
また，Python⇔JS間で連携できる点はもっと有効活用できそうに感じています．

英語プレゼンに悩んでいる方はDeepLopenerOCR，試してみてください．

https://github.com/T3aHat/DeepLopenerOCR