fortune を ChatGPT で翻訳して繰り返し表示するピングー掲示板

## YouTube 動画

タイトルだけでは何言ってるのかわからないと思うので、録画して YouTube に上げた。やっているのは、

- fortune コマンドの出力を
- repeat コマンドで繰り返し
- ChatGPT で動的に翻訳して
- 全体をフレームで囲み
- ピングー化して
- スクロール領域を設定して

表示すること。

```sh
optex -Mscroll --line 20 -- -Mpingu repeat -i 30 10 fortune --frame --ja
```

30秒間隔で10回出力しているので、全部見ると6分弱の映像になっている。

<!--
![pingu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/4925b52e-1db1-92d5-ef3f-b36495b8428b.png)
-->

[<img width=600 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/4925b52e-1db1-92d5-ef3f-b36495b8428b.png">](https://youtu.be/YKJM8sz5yvw?si=Z6yHRx6wEtV7wpMw)

以下、ここで使っている技術について解説します。

## optex -Mpingu

optex コマンドの `-MPingu` モジュール。

https://github.com/kaz-utashiro/optex-pingu

Pingu 化の元ネタはこちら。メディアでも取り上げられたので見た方も多いと思う。

https://github.com/sheepla/pingu

https://forest.watch.impress.co.jp/docs/serial/yajiuma/1416765.html

`-Mpingu` モジュールは `ping` だけではなく、あらゆるコマンドを Pingu 化するものだ。最初は pingu コマンドの画像データを拝借してそのまま使っていたのだが、最近各文字を上下に分割して縦方向の解像度を倍増した。

![pingu2-light.png](https://raw.githubusercontent.com/kaz-utashiro/optex-pingu/refs/heads/main/images/pingu2-light.png)

pingu モジュールは、読み込んだデータを1行ずつ、デフォルトでは0.1秒間隔で出力する。

## optex -Mscroll

https://github.com/kaz-utashiro/optex-scroll

optex の `-Mscroll` モジュールは、端末にスクロール領域を設定する。デフォルトでは10行だが、この例では `--line 20` というオプションを指定して20行の領域を指定している。

`docker build` コマンドや各種のインストーラーでこのような手法が使われているので、汎用化できないかと実装したものだ。

## .optex.d/bin/repeat

https://github.com/kaz-utashiro/optex?tab=readme-ov-file#command-aliases

`repeat` コマンドは、その後に指定したコマンドを繰り返し実行する。Cシェルの組み込みコマンドで便利に使っていたのだが bash に切り替えてからなくなって不便に思っていたので optex の alias 機能を使って実装した。`-i` オプションでインターバルタイムを指定できる。

```~/.optex.d/config.toml
[alias]
        repeat = [ 'bash', '-c', '''
            while getopts 'c:i:px' OPT ; do
                case $OPT in
                    c) count=$OPTARG ;;
                    i) sleep=$OPTARG ;;
                    p) paragraph=yes ;;
                    x) set -x ;;
                esac
            done; shift $((OPTIND - 1))
            case $1 in
                [0-9]*) count=$1 ; shift ;;
            esac
            : ${count:=1}
            command=("$@")
            while ((count--)) ; do
                eval "${command[@]@Q}"
                if (( count > 0 )) ; then
                    [ "$paragraph" ] && echo
                    [ "$sleep" ] && sleep $sleep
                fi
            done
        ''', 'repeat' ]
```

`~/.optex.d/bin` には `repeat` という名前で `optex` へのシンボリックリンクが置いてある。optex は `config.toml` を読んで上のシェルスクリプトを実行するという仕組みだ。もちろん repeat という名前で同じ内容のスクリプトを置いても構わない。

ただ、こうしておくと `optex --rm repeat` のようにしてシンボリックリンクを消してしまえばパスから外すことができて便利な場合もある。

## .optex.d/bin/fortune

fortune コマンドは homebrew でインストールしたものだが、これも optex を通して実行することで `--frame` と `--ja` というオプションを実現している。

```~/.optex.d/fortune.rc
option --frame -Mutil::filter \
        --of 'ansicolumn -C1 --border=heavy-box -P20 -S85'

define @AREA "(^\S.*\n)+|(^\h+\S.*\n)"
option --ja -Mutil::filter \
        --of 'xlate -t JA -sa -o space+ -w80 -e gpt4o -p @AREA'
```

`-Mutil::filter` モジュールで `--of` というオプションが定義されていて、出力フィルターとするコマンドを指定することができる。

## ansicolumn

https://github.com/tecolicom/App-ansicolumn

fortune コマンドに `--frame` オプションを指定すると、出力を `ansicolumn -C1 --border=heavy-box -P20 -S85` コマンドを経由して表示する。これは 20x85 の大きさで `heavy-box` というスタイルのボーダーを付けて1カラムで表示するという指定だ。

## xlate

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

fortune コマンドに `--ja` オプションを指定すると、出力を `xlate` コマンドに送る。`xlate` は bash スクリプトで `App::Greple::xlate` モジュールの配布に同梱されてるフロントエンドの CLI だ。

ここで指定している `xlate` のオプションは以下のようなもの。

- `-t JA`: 日本語に翻訳
- `-s`: 進行状況を表示しない (silent)
- `-a`: API を使って翻訳する
- `-o space+`: 原文と翻訳文の後に空白行を出力する形式
- `-w80`: 80文字で折り返す
- `-e gpt4o`: 翻訳エンジンとして ChatGPT-4o を使う
- `-p @AREA`: 翻訳対象の領域に `@AREA` を指定する

`@AREA` は上の行で `(^\S.*\n)+|(^\h+\S.*\n)` のように定義している。これは、空白以外の文字で始まる行の連続 `(^\S.*\n)+` と、空白で始まる行 `^\h+\S.*\n` という意味だ。つまり、先頭から始まる文の連続はパラグラフとしてまとめて翻訳し、空白で始まる行は1行ずつ独立して翻訳する。

このパターンを `greple` コマンドに指定すると、どの部分が翻訳対象になるかがわかる。この例では、連続する行をまとめて翻訳しない方がいいのだが、これを自動的に認識するのは難しい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/c6a47d70-d26c-7615-4558-c9c81b15c8bc.png)

fortune の出力は、文章の後にインデントして出典が書かれていることが多いので、このようなパターンでだいたい思ったように翻訳してくれる。もちろん例外はあるので、時々おかしな内容になってしまうこともある。

この例では `gpt4o` を使って翻訳しているが DeepL を使うことも可能だ。

## まとめ

ここ数日 optex の `pingu` と `scroll` モジュールを更新していた。このように optex を使って実行するコマンドを連鎖的に利用するのは初めてで、実装を見直す必要があった。greple の `xlate` モジュールや `ansicolumn` を組み合わせることで、ある意味集大成的な応用例になっていると思う。

実は、まだどうもうまく動いていない部分がある。

    optex -Mpingu fortune --frame

のように実行すると、fortune コマンドの終了を待たずにプロンプトが帰ってきてしまうのだ。

    optex -Mpingu env fortune --frame

のように `env` や `sh -c` を介して実行すると問題ないので、プロセスグループの管理が絡んでいるのではないかと思っている。原因や対処法がわかる方がいましたらアドバイスお願いします。