Docker  だけで使える DeepL API による自動翻訳環境

[オープンデータ基本指針]: https://www.digital.go.jp/assets/contents/node/basic_page/field_ref_resources/f7fde41d-ffca-4b2a-9b25-94b8a701a037/20210615_resources_data_guideline_02.docx
[DeepL Python]: https://github.com/DeepLcom/deepl-python
[Greple::xlate]: https://github.com/kaz-utashiro/App-Greple-xlate
[ansicolumn]: https://github.com/tecolicom/App-ansicolumn
[optex]: https://github.com/kaz-utashiro/optex
[sdif]: https://github.com/kaz-utashiro/sdif-tools


少し前にこういう記事を書いた。

https://qiita.com/kaz-utashiro/items/1c1a51a4591922e18250

https://qiita.com/kaz-utashiro/items/a5e19736416ca183ecf6

ファイルの一部を DeepL の API を使って自動的に翻訳するためのモジュールと、それを使って自身のドキュメントを15ヶ国語に翻訳したという内容だ。その後も少しずつドキュメントは修正しているが、リリースコマンドを実行するだけで1分もかからずに15ヶ国語の内容を更新してくれるので、なんの苦労もなくメンテナンスができている。これを人手でやろうと思ったら大変なことだ。もっとも、翻訳したものを読んでいる人が一人でもいるのかが疑問ではあるのだが。

多国語に翻訳するためには make を活用している。汎用的な Makefile と、定型的な変換ルールを定義することで、最小限の設定で簡単に使える自動翻訳環境が提供できるのではと考えた。結果として `xlate` という bash スクリプトができた。リポジトリはこちら。

https://github.com/kaz-utashiro/App-Greple-xlate

DeepL API を使うことが前提なので、API の利用登録をして `DEEPL_AUTH_KEY` という環境変数に利用するための鍵が設定されていなければならない。また、事前に [DeepL Python] のインストールが必要。API を使わない方法もあって、最後に簡単に紹介する。

## 1分で体験する

気の短い人のために、Docker 環境があれば1分で一通り何が起こるか体験する方法を紹介しておく。リポジトリ中の examples というフォルダーにサンプルデータを置いてある。次のようにすれば、実際に動く様子を見ることができる。すべての処理は Docker 上で実行するので、コマンドのインストールや環境設定は不要だ。このフォルダーにはキャッシュファイルも置いてあって、 DeepL のサービスも使う必要はないので、処理は一瞬で終わるはず。イメージは若干大きいが、ダウンロードする時間も含めて1分程度で終わると思う。

```sh
# リポジトリをクローンする
$ git clone https://github.com/kaz-utashiro/App-Greple-xlate.git
# examples のディレクトリに移動する
$ cd App-Greple-xlate/examples
# ../script にある xlate コマンドで自動生成されるファイルを削除する
$ ../script/xlate -DM clean
# -Mn オプションで実行されるコマンドを見ることができる (ファイル名は指定しなくてもいい)
$ ../script/xlate -DMn data_shishin.docx
optex -Mtc cat data_shishin.docx > data_shishin.stxt
xlate -t EN-US -o xtxt  data_shishin.stxt > data_shishin.EN-US.xtxt
xlate -t EN-US -o cm    data_shishin.stxt > data_shishin.EN-US.cm
xlate -t EN-US -o ifdef data_shishin.stxt > data_shishin.EN-US.ifdef
xlate -t FR    -o xtxt  data_shishin.stxt > data_shishin.FR.xtxt
xlate -t FR    -o cm    data_shishin.stxt > data_shishin.FR.cm
xlate -t FR    -o ifdef data_shishin.stxt > data_shishin.FR.ifdef
xlate -t KO    -o xtxt  data_shishin.stxt > data_shishin.KO.xtxt
xlate -t KO    -o cm    data_shishin.stxt > data_shishin.KO.cm
xlate -t KO    -o ifdef data_shishin.stxt > data_shishin.KO.ifdef
# 実際には、上のコマンドはこのように実行して出力を見やすく整形している
# ../script/xlate -DMn data_shishin.docx | teip -g ^xlate -- ansicolumn -t -o ' '
# 実行する
$ ../script/xlate -DM data_shishin.docx
```

`xlate -Mn` の行は、結果をわかりやすくするために `teip` と `ansicolumn` というコマンドを使って整形している。`teip` は、データの一部をフィルターに送るもの。`ansicolumn -t` は、各項目を縦に揃えるもの。`column -t` でもよい。

`data_shishin.docx` という Word ファイルからテキストを抽出して、英語、フランス語、韓国語の三カ国後に翻訳している。出力は、内容を翻訳文に置き換えた `.xtxt` と、対訳形式の `.cm` と `.ifdef` という2種類のファイルを作っている。他にもファイルがあるので、この例では処理するファイル名を指定しているが、何も書かなければすべてのファイルを処理する。だから、やることといえば、翻訳したいファイルを置いて `xlate -DM` を実行するだけだ。

その場で `../script/xlate -GC` を実行すると、Git のリポジトリをマウントして bash が立ち上がる。日本語の文書を管理するための Git 環境が整えてあるので、もう少し時間があったら眺めてみるといいと思う。

```sh
$ ../script/xlate -GC
root@xlate:/work/examples#
root@xlate:/work/examples# git log -p data_shishin.docx
```

## Docker と bash スクリプトだけで翻訳する

`greple -Mxlate` モジュールは、greple コマンドの拡張として汎用的に作られているため、実用的な目的に使おうとすると、いろいろとオプションを指定しなければならない。だから、簡単に使うためのフロントエンドとして `xlate` という bash スクリプトを作成した。まずは、この `xlate` コマンドと Docker を使うことで、設定や環境整備が不要な自動翻訳環境を使う方法を紹介してみる。

翻訳対象はテキストファイル以外に Word ファイルにも対応している。元々テキストファイルのために作った環境だが、テキストの場合自由度が高すぎて翻訳対象部分を限定するのが案外煩雑だ。だから、デモとしては Word ファイルの方が適していたりもする。[デジタル庁](https://www.digital.go.jp/resources/open_data/)が「[オープンデータ基本指針]」という文書を Word 形式で公開しているのでそれを使おう。名前が長いので `example.docx` というファイル名にする。

```sh
curl -o example.docx https://www.digital.go.jp/assets/contents/node/basic_page/field_ref_resources/f7fde41d-ffca-4b2a-9b25-94b8a701a037/20210615_resources_data_guideline_02.docx
```

次に `tecolicom/xlate` のコンテナイメージを持ってくる。

```sh
docker pull tecolicom/xlate
```

`xlate` コマンドを取得する方法はいくつかあるが、すでに Docker イメージがあればこんな風にするのが簡単だ。

```sh
docker run --rm tecolicom/xlate cat /usr/local/bin/xlate > xlate
chmod +x xlate
```

まず、`xlate` コマンドが使えるか試してみよう。次のように `./xlate -C ls` と実行してみる。

```sh
$ ./xlate -C ls -1
Unable to find image 'tecolicom/xlate:0.19' locally
0.19: Pulling from tecolicom/xlate
Digest: sha256:ce19bcc198143d19ff56fcd47ac0cf1f92f777a696a579f3d72fac1ebead5fd7
Status: Downloaded newer image for tecolicom/xlate:0.19
example.docx
xlate
```

`-C` は、Docker コンテナ上でコマンドを実行するオプションだ。`xlate` コマンドには対応するイメージのバージョンが組み込まれていて、この例では `:0.19` をダウンロードしている。といっても、これは `:latest` と同じはずなので、新しい名前をつけるだけ。2回目以降はこのメッセージは表示されない。ちなみに、今記事を更新している時点のバージョンは `:0.23` だ。

ディレクトリには `example.docx` と `xlate` の2つのファイルだけがある。ここで `xlate -DMa` というコマンドを実行する。オプション `-D` は Docker コンテナ、`-a` は DeepL API を使うというもの。`-M` を付けるとデフォルトの make ルールが適用され、`xtxt` と `cm` という2つの形式で米語 (`EN-US`) に変換したファイルを作成する。



```sh
$ xlate -DMa
optex -Mtc cat example.docx > example.stxt
xlate   -a -t EN-US -o xtxt example.stxt > example.EN-US.xtxt
From:
	< オープンデータ基本指針
	< 平成29年５月30日
...
To:
	> Open Data Basic Guidelines
	> May 30, 2009
...
write cache to example.stxt.xlate-deepl-EN-US.json
xlate   -a -t EN-US -o cm example.mstxt > example.EN-US.cm
read cache from example.stxt.xlate-deepl-EN-US.json
```

まず `example.stxt` というファイルを作る。これは、docx からテキストだけを抜き出したものだ。次に、英語の翻訳した `example.EN-US.xtxt` というファイルを作成する。はじめての実行なので、すべてのテキストが翻訳され、原文と翻訳結果がすべて表示される。実行には数秒間待たされる。

次に `example.EN-US.cm` というファイルを作るが、この時はすでに前回の処理の結果がキャッシュされているので、処理は一瞬で終わり経過も表示されない。

```sh
$ ls -1sh
total 200K
48K example.EN-US.cm
24K example.EN-US.xtxt
60K example.docx
24K example.stxt
44K example.stxt.xlate-deepl-EN-US.json
```

`.cm` ファイルの方には次のようなデータが入っている。以下のように git で使われている conflict marker 形式で、原文と訳文を併記している。

> `<<<<<<< ORIGINAL`
> 　我が国においては、平成23年３月11日の東日本大震災以降、政府、地方公共団体や事業者等が保有するデータの公開・活用に対する意識が高まった。
> `=======`
> In Japan, since the Great East Japan Earthquake on March 11, 2011, there has been an increased awareness of the need to disclose and utilize data held by the government, local governments, and businesses.
> `>>>>>>> EN-US`

## Docker 環境でコマンドを実行する

Docker 環境には、翻訳する以外にも各種の周辺ツールが含まれている。たとえば conflict marker 形式のファイルを見やすく表示するために `sdif` コマンドが利用できる。ちなみに、DeepL のサービスを使えば、Word のファイルを丸ごと翻訳してもらうこともできるので、翻訳結果だけあればいいという人はそれを使えばいいと思う。

上の conflict marker 形式も原文と訳文を比較するという意味では十分なのではあるが、訳文を読んでいて、わからない箇所があったら原文に当たるというような用途にはあまり適してはいない。[sdif] コマンドを使うと、原文と翻訳結果を並べて見ることができる。

```sh
xlate -C sdif -V --nocdif example.EN-US.cm
```

![スクリーンショット 2023-03-22 17.16.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/a0d74ec0-e4d5-7a3d-26ca-fb2511fd642c.png)

`-C` オプションの後にコマンドを指定すると、それを Docker のコンテナ上で実行することができる。[sdif] の `-V` オプションは、diff 以外のテキストを新旧両方のテキストとして扱うことを指示するものだ。`--nocdif` は、単語単位での差分を出さないためのもの。ドキュメントの更新時には役に立つのだが、このように左右がまったく違う場合にはうるさいだけで意味がない。

## Docker 環境でシェルを使う

気の早い人は、上のコマンドをパイプで `less` に流そうとして悩んでいるのではないだろうか。`xlate` コマンドは `docker` を `-it` オプション付きで実行するので、リモート側とホスト側で端末入出力の取り扱いがうまくいかなくておかしなことが起こる。`-B` オプションをつけると、`-it` を付けなくなるが今度は端末情報の取得がうまくいかなくなる。

一つの解決方法は、次のように `less` も Docker 上で実行してしまうことだ。

```sh
xlate -C bash -c 'sdif -V --nocdif example.EN-US.cm | less'
```

ただ、いちいち `bash -c` とか書くのは面倒だし、実は `less` を使うためにも若干環境設定が必要になったりもする。だったら、いっそのこと Docker 上でシェルを実行してしまったらどうか。やり方は簡単で `-C` の後に何も書かなければいい。こんなふうに見えていれば成功だ。

![スクリーンショット 2023-03-22 18.05.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/f9e57f7f-5e74-3c1e-9526-11242632be99.png)

自動的にカレントディレクトリが `/work` にマウントされている。ここで、次のように実行すればよい。

```sh
sdif -V --nocdif example.EN-US.cm | less
```

他にも、いろいろとコマンドがインストールされている。単純にファイルを並べて表示したければ、こんな風にもできる。

```sh
ansicolumn -D example.stxt example.EN-US.xtxt | less
```

![スクリーンショット 2023-03-22 17.44.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/ce5ec79a-d11c-6fdf-db37-09c2a639252d.png)

[ansicolumn] は、データをマルチカラムで表示するためのコマンドだが、複数のファイルを指定すると、それらを並べて表示する。ただし、[sdif] と違って、対応する箇所を並べて表示したりはしない。`-D` は、行末の折り返しをよろしくやってくれるオプションだ。詳しくは[こちらの記事](https://qiita.com/kaz-utashiro/items/32e1c2d4c42a80c42422)をどうぞ。

## Docker 環境で Git を使う

Docker コンテナには git 環境が一式入っている。日本語のテキストファイルを diff する場合には、`mecab` で形態素解析した単位で比較した差分結果を表示する。文章ファイルを比較しようとすると、このような環境がないと気持ちよく読めない。

Docker 上で git 環境を使う場合には、`-C` ではなく `-GC` オプションを使う。単なる `-C` だと、カレントディレクトリを `/work` にマウントするのだが、`-G` オプションがあると git のルートディレクトリをマウントする。そうしないと、リポジトリデータにアクセスできないからだ。

```sh
$ xlate -GC
Mount /Users/tecolicom/App-Greple-xlate to /work
root@xlate:/work/public/xlate/examples#
```

テキスト以外のファイルも可能なものはテキストに変換して比較するように設定してあるので、次のように  Word ドキュメントの差分表示も可能だ。

![スクリーンショット 2023-03-22 20.09.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/d63414eb-448d-4d5f-cb2e-ff9d7dba2c6b.png)

conflict marker 形式のファイルを git で管理していると、原文と訳文の更新を同時に見ることができて案外役に立ちそうだ。次の例は、オープンデータ基本指針の原文と訳文が、改訂によってどう変化したかがわかりやすく表示されている。

![スクリーンショット 2023-03-25 18.39.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/3d10a19d-5e9c-801a-0495-61ce1eef9a60.png)

## その他の機能

基本的な使い方については大体紹介できたと思う。xlate コマンドの詳細なマニュアルはまだ書いていないのだが、簡単な使い方は `xlate -h` で見ることができる。その他の機能についても機会を改めて紹介したいと思うが、とりあえず簡単に書いておく。仕様に関しては、これから変わるかもしれないし、提案や要望があれば採り入れさせていただきたい。

### 翻訳言語や形式を指定する

#### *.LANG

xlate のデフォルトでは、翻訳元の言語は自動認識して、米語 (EN-US) に翻訳する。翻訳先の言語を指定するためには　`ファイル名.LANG` というファイルを作っておけばいい。上の例であれば `example.docx.LANG` というファイルに `EN-US KO ZH` と書いてあれば、米語、韓国語、中国語の3ヶ国語に変換する。

#### *.FORMAT

翻訳形式は `ファイル名.FORMAT` というファイルで指定する。この中には `xtxt`、`cm`、`ifdef` という3種類の形式を記述することができる。

#### -t LANG -o FORMAT オプション

ファイルに対して何度も同じ処理を繰り返すのであれば、上のようにファイルに記述しておくのがいいだろう。その必要がなければ、オプションで指定することもできる。次のように実行すると、他にファイルがあっても `example.docx` のみを対象にして、3ヶ国語で2つの形式のファイルを生成する。

```sh
xlate -M -o 'xtxt ifdef' -t 'EN-US KO ZH' example.docx
```

複数の値を指定できるのは `-M` オプションがある時だけということに注意してほしい。

### 翻訳結果はキャッシュされる

一度翻訳した結果は JSON 形式のファイルにキャッシュされ、次回以降はそのキャッシュにないデータのみが翻訳処理に送られる。上の例であれば `example.docx.xlate-deepl-EN-US.json` というファイルが自動的に作成される。

何か翻訳したいファイルがあったら、とりあえず xlate コマンドで処理してみればいい。たとえば、何かの README.md ファイルを日本語に翻訳したければ、次のように実行してみる。

```sh
xlate -t JA README.md
```

結果は標準出力に出るので流れていってしまうが、おおよそどんなことが起こるか把握することができる。しかし、翻訳処理の結果はキャッシュに残っているので、それをどのような形で活用するかは、それからゆっくり考えればいい。

### API を使わずに翻訳する

上の説明で xlate に `-a` オプションを付けると API を使うと書いた。つまり `-a` オプションがなければ API を使わない。どうなるかというと、翻訳するテキストがなくなるまで次の処理を繰り返す。

- 翻訳テキストをクリップボードにコピーしたところでプロンプトを表示する
- 自分でブラウザやアプリの DeepL にペーストして翻訳して、結果をコピーする
- エンターキーを入力すると処理が進む

無料アカウントでは、翻訳できる文字数の上限が5000と決まっているので、5000文字単位でこの操作を繰り返すことになる。有料アカウントであればもっと多くのテキストを処理することができるので、その場合は `-m` オプションで上限値を指定する。

この処理は Docker 上でもできる。ただ、コンテナの X11 環境とホストのクリップボードの連携が課題となる。手元の macOS (Monterey) の Xquartz を使った場合、連携するように設定してもうまくいかないので `xclip -o | pbcopy` みたいなことをしなければならない。解決する方法があればぜひ教えてほしい。それ以外の環境ではテストしていない。

API を使わない場合には、次のようにローカル環境で使う方がいい。

### ローカル環境にインストールする

今回は Docker コンテナ上で実行することを中心に書いたが、ローカル環境ですべての操作を行うことももちろんできるし、そちらの方がむしろ想定した環境だ。

```sh
cpanm -n App::Greple::xlate
```

すれば　`xlate` コマンドは使えるようになる。API を使わないのであれば、これだけで基本的な処理はできるはずだ。API を使うのであれば、次のようにして `deepl` コマンドをインストールする。

```sh
pip install --upgrade deepl
```

それ以外の環境はリポジトリの Dockerfile を参考にしてほしい。

### Emacs から使う

[xlate.el]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/share/xlate.el

Emacs から使うためのインタフェースも xlate コマンドを使うように変更した。[xlate.el] をロードすると `xlate-region` という関数が使えるようになる。プレフィックス (Control-U) を付けて実行すると、言語を指定することができて、次回以降はそれがデフォルトになる。

ちなみに、以前のバージョンでは言語がハードコーディングしてあったのを ChatGPT に頼んで修正してもらった。