emacsのライティング環境整備（無料APIを使った翻訳、辞書、llmクライアント）

## はじめに
ここのところ、わたくしのemacsは[consult](https://github.com/minad/consult) + [corfu](https://github.com/minad/corfu) + [vertico](https://github.com/minad/vertico) + [marginalia](https://github.com/minad/marginalia) + [orderless](https://github.com/oantolin/orderless) + [embark](https://github.com/oantolin/embark) + recentfという安定の補完環境で絶賛稼働中です。
いずれのパッケージもgithubでのスター数が1,200前後なので、これらの組み合わせが2025年のメインストリームなのでしょうね（anythingやhelmの時代から大きく進化しました。あとは本家のeglot/tree-sitterの挙動がもう少し安定してくれれば文句がないのですが）。

さて、春の訪れにあわせて、しばらく放置していたライティング環境を整備してみました。
具体的には、辞書と翻訳、それに対話型llmクライアントです。一連のパッケージに必要なAPIについては、いずれも無料のものを使用しました。無料APIなので、キーはinit.elに直接記載してあります（文中では`hoge-fuga`と表記）。

環境はArch Linux(6.13.5-2) + Xfce4(4.20) + emacs(30.1-3)です。

## 辞書

### sdcv

#### 前提
- sdcvおよび辞書をインストール済みで、コンソール上で安定動作していること
- melpaから[sdcv.el](https://github.com/Dushistov/sdcv)を導入してください
- 使用する辞書のpathは各自の環境に合わせてください（サンプルでは英和および日本語辞書を設定）

#### my init.el
```emacs-lisp:init.el
;; == sdcv ==
(require 'sdcv)
(require 'posframe)
(setq sdcv-dictionary-data-dir "~/.stardict/dic")
(setq sdcv-dictionary-complete-list
      '("EIJIRO v106"
        "新明解国語辞典"
        "広辞苑　第六版"))
(global-set-key (kbd "C-c j") 'sdcv-search-pointer)
(global-set-key (kbd "C-c J") 'sdcv-search-input)
(setq sdcv-word-pronounce -1)
```
#### 使用方法
- `Ctrl-c j`でカーソル位置の単語を検索
- `Ctrl-c J`で検索したい単語を入力

### goldendict

#### 前提
- goldendictにはたくさんのforkがあって、どれがベストなのかよくわかりません
- わたくしは`aur/goldendict-ng-git 24.11.0.r5693.15207cf4-1`を使用中です
- goldendictアプリを導入済みで、デスクトップで安定動作していること
- パッケージの導入は不要です（下記の関数で呼び出します）

#### my init.el
```emacs-lisp:init.el
;; == goldendict ==
(defun look-up-dict (word)
  (start-process "goldendict" nil "goldendict" word))

(defun look-up-dict-marked ()
  (interactive)
  (let ((word ""))
    (if (equal major-mode 'pdf-view-mode)
        (setq word (car (pdf-view-active-region-text)))
      (setq word  (buffer-substring (mark) (point))))
    (look-up-dict word)))

(global-set-key (kbd "C-; w") 'look-up-dict-marked)
```

#### 使用方法
- 検索したい単語を領域選択後、`Ctrl-; w`でpopupの検索結果が開きます
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385798/e5ee08c1-4119-4329-85b0-0923f91e7062.png)

## 翻訳

### DeepL

#### 前提
- 様々な実装を試してみましたが、"[EmacsからDeepLのAPIを叩くコマンドを実装する](https://qiita.com/masatoi0/items/26c7384749393a1bf8fb)"がわたくしのスタイルに最も合致していました
- コード本体とインストール方法については、上記ページをご参照ください

#### my init.el
```emacs-lisp:init.el
;; == DeepL EN ==
(use-package request)
(load-file "~/.emacs.d/mypac/deepl.el")
(global-set-key (kbd "C-c q") 'deepl-translate)
(setq deepl-auth-key "hoge-fuga")
```
#### 使用方法
- 検索したい文章を領域選択後、`Ctrl-c q`でミニバッファに翻訳結果が表示され、同時にクリップボードにもコピーされます

### DeepL ブラウザ版

#### 前提
- こちらは検索結果がブラウザ上で開きます
- melpaから`url-util`パッケージを導入してください

#### my init.el
```emacs-lisp:init.el
;; == DeepL without API ==
(require 'url-util)
(defun deepl-noapi (&optional string)
  "DeepL browser version"
  (interactive)
  (setq string
        (cond ((stringp string) string)
              ((use-region-p)
               (buffer-substring (region-beginning) (region-end)))
              (t
               (save-excursion
                 (let (s)
                   (forward-char 1)
                   (backward-sentence)
                   (setq s (point))
                   (forward-sentence)
                   (buffer-substring s (point)))))))
  (run-at-time 0.1 nil 'deactivate-mark)
  (browse-url
   (concat
    "https://www.deepl.com/translator#en/ja/"
    (url-hexify-string string)
    )))
(global-set-key (kbd "C-; y") 'deepl-noapi)
```
#### 使用方法
- 検索したい文章を領域選択後、`Ctrl-; y`でブラウザの新規タブが開き、結果が表示されます
![000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385798/f506caf0-723c-40fd-a921-84e733898638.png)

### go-translate

#### 前提
- 著名な[go-translate](https://github.com/lorniu/go-translate)です
- 複数言語を複数エンジンで同時に翻訳し、別バッファに結果を表示してくれます
- melpaからgo-translate.elを導入してください

#### my init.el
```emacs-lisp:init.el
;; == go-translate ==
(use-package go-translate
  :ensure t
  :bind ("C-c t" . gt-do-translate)
  :config
  (setq gt-langs '(ja en it)) 
  (setq gt-default-translator
      (gt-translator
       :taker   (gt-taker :text 'buffer :pick 'paragraph)
       :engines (list
                 (gt-google-engine)
                 (gt-bing-engine)
                 (gt-deepl-engine :key "hoge-fuga" :pro nil)
                 )
       :render  (gt-buffer-render))))
```

#### 使用方法
- 検索したい文章を領域選択後、`Ctrl-c t`で新規バッファが開き、結果が表示されます
- バッファ上で`t`を押すと翻訳方向が切り替わります（サンプルでは[en]->[ja] ⇒ [en]->[it] ⇒ [it]->[ja] ⇒ [it]->[en]のサイクル）

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385798/bd4a7a51-ba74-4a37-bfbd-727437d11dc6.png)

## llmクライアント
ライティング環境で気軽に利用できる対話型のクライアントとして、現時点では[gptel](https://github.com/karthink/gptel)がわたくしには最適です。

### gptel

#### 前提
- 活発に開発が続いており、メジャーなllmのほとんどに対応しています
- テキストやコーディング環境だけでなく、org-modeからも、別バッファを開くことなくシームレスにllmエンジンを呼び出すことが可能です
- サンプルでは3つのllm（いずれも無料APIを利用)を登録しています。詳しい設定については[gptel](https://github.com/karthink/gptel)をご参照ください
- melpaからgptel.elを導入してください

#### my init.el
```emacs-lisp:init.el
;; == gptel ==
(require 'gptel)
(setq gptel-default-mode 'org-mode)
(global-set-key (kbd "C-c g") 'gptel-mode)
(global-set-key (kbd "C-c G") 'gptel-menu)

;; Github as default
(setq gptel-model  'gpt-4o
      gptel-backend
      (gptel-make-openai "Github Models"
        :host "models.inference.ai.azure.com"
        :endpoint "/chat/completions?api-version=2024-05-01-preview"
        :stream t
        :key "hoge-fuga"
        :models '(gpt-4o)))

;; Gemini as option
(gptel-make-gemini "Gemini" :key "hoge-fuga" :stream t)

;; Groq as option
(gptel-make-openai "Groq"
  :host "api.groq.com"
  :endpoint "/openai/v1/chat/completions"
  :stream t
  :key "hoge-fuga"
  :models '(llama-3.1-70b-versatile
            llama-3.1-8b-instant
            llama3-70b-8192
            llama3-8b-8192
            mixtral-8x7b-32768
            gemma-7b-it))
```

#### 使用方法
- たとえば、org-modeでファイルを編集中に`Ctrl-c g`を押すと、同一バッファ上でGptelのminor modeが起動します
- `Ctrl-c <Enter>`で質問内容をllmに送ると、回答が表示されます

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385798/9099f852-de8a-41df-a128-5f46d8789121.png)
- `Ctrl-c G`を押すと、ミニバッファーでメニューが開き、別エンジンの選択や表示形式の変更などが可能になります

![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385798/6f5525b3-ada4-4d80-a454-fb9b0421d3f3.png)

## おまけ

### radio.el
[radio.el](https://github.com/jroimartin/radio)は、emacs上でmpvを介してstreamingラジオを聞くためのシンプルなパッケージです。
一ヶ月前、melpaに登録されてすぐに使い始めました。
直近のバージョンが不安定だったので、いまはmelpaを使わず、ver 0.2.1のコードを直接loadしています。ラジオ局の選択と切断だけのミニマルな環境が心地良いです。

#### 前提
- melpaからradio.elパッケージを導入するか、[作者のgithub](https://github.com/jroimartin/radio)にある適当なソース（サンプルではver 0.2.1）をローカルに保存します
- mpvアプリが機能していること
たとえば`mpv https://live.amperwave.net/direct/ppm-jazz24mp3-ibc1`で音が出ることを確認してください
- あとはinit.elにお気に入りのラジオ局を追加してください
#### my init.el
```emacs-lisp:init.el
;; == radio ==
(load-file "~/.emacs.d/mypac/radio.el")
(customize-set-variable
 'radio-command
 '("mpv" "--terminal=no" "--video=no" ":url"))

(customize-set-variable
 'radio-stations-alist
 '(
   ("Ambiente" . "http://uk2.internet-radio.com:8171/listen.pls")
   ("Jazz 24" . "https://live.amperwave.net/direct/ppm-jazz24mp3-ibc1")
   ("Lounge" . "http://stream.klassikradio.de/lounge-beat/mp3-128/")
   ("ESTILO LEBLON" . "http://us4.internet-radio.com:8193/")
   ("BBC 1" . "http://as-hls-ww-live.akamaized.net/pool_01505109/live/ww/bbc_radio_one/bbc_radio_one.isml/bbc_radio_one-audio%3d96000.norewind.m3u8")
   ))

(global-set-key (kbd "C-c r") 'radio)
(global-set-key (kbd "C-c R") 'radio-stop)
```
#### 使用方法
- `Ctrl-c r`で聴きたいラジオ局を選択します
- `Ctrl-c R`で放送を切断します
-----
以上、お役に立てば幸いです。