« ^ »

DeepL APIを使う

所要時間: 約 4分

Google翻訳が出てきた時にはこんな便利なものがあるのかと思ったが、不自然な文章に翻訳されるといった事もしばしばあった。そこから時が経ち、DeepLが出てきた。とても自然な文章に翻訳されて凄いと関心した。その頃にはGoogle翻訳も進化していて、不自然な文章は、ほとんど出力されなくなっていた。最近はChatGPTをはじめとした生成系AIのサービスの方が、翻訳の精度も高いような気もする。「翻訳して」とお願いすれば、前後の文脈を翻訳してくれる。

DeepLの良い所の1つにAPIが無料という事がある。フリープランの場合、500000文字まで無料で使用できる。OpenAI APIの場合は消費トークン毎に課金されるため、単純な翻訳などはDeepLにも流せるようにしたい。そこで今回はEmacsからDeepL APIを呼び出して、翻訳ができるようにする事にした。

クレジットカードを登録する

APIキーの取得のためにはクレジットカードが必要になる。これが面倒で今まではWeb UIで使用していたが、この機会に登録する事にした。登録すると0ユーロの支払いを求められた。

クレジットカードの登録は、同一のユーザーが複数のフリープランのアカウントを作成して、500000文字を越える翻訳を行えないようにするための措置らしい。

翻訳API

用意されているAPIはいくつかある。今回は文字の翻訳のみできればよいので /v2/translate だけ確認する。APIの資料はhttps://www.deepl.com/docs-apiにある。

:ORIGIN := deepl-origin
:DEEPL_AUTH_KEY := deepl-auth-key

POST :ORIGIN/v2/translate
Content-Type: application/json
Authorization: DeepL-Auth-Key :DEEPL_AUTH_KEY

{"text":["日本語から英語への翻訳の場合はどうなるのか?"],"target_lang":"en"}
{
  "translations": [
    {
      "detected_source_language": "JA",
      "text": "What happens in the case of translation from Japanese to English?"
    }
  ]
}
// POST https://api-free.deepl.com/v2/translate
// HTTP/1.1 200 OK
// date: Sat, 02 Mar 2024 01:27:38 GMT
// content-type: application/json
// transfer-encoding: chunked
// vary: Accept-Encoding
// access-control-allow-origin: *
// strict-transport-security: max-age=63072000; includeSubDomains; preload
// server-timing: l7_lb_tls;dur=966, l7_lb_idle;dur=410, l7_lb_receive;dur=0, l7_lb_total;dur=1061
// access-control-expose-headers: Server-Timing
// Request duration: 2.143655s

既にあるEmacs拡張

EmacsからDeepLを使うための拡張はいくつかある。

探せば、おそらくもっとあるだろう。特にこだわりがなければ、これらを使うのが良さそうだ。

僕は、自分が使う機能以外のコードをできるかぎり持ちたくないのと、やりたい事が増えた時、ライブラリの使い方を調べたりしたくないので、自分で実装する事にする。

自分で必要なぶんだけ実装するのは少し手間だけれど、=curl= でのやり方は分かるのに、ライブラリがそれらをどのように扱っているか分からなくてドギマギするという事がなくなる。

この手の拡張を書く時、Emacsでは url-retrieve.elplz.elrequest.el を使っているライブラリを良く見るが、curlがあるなら make-process を使い curl を呼び出して HTTP 通信を行う方が良いのではないかと思っている。シェルで動作を確認しやすいし、エラー発生時のデバッグもやりやすい。

curl が無い環境はどうするんだという話もあるんだけれど、うん、curlをビルドすれば良いんじゃないかな。

Emacs拡張

僕が実装したオレオレEmacs拡張を置いておく。

deepl.el

;;; deepl.el --- Elisp library for the DeepL API  -*- lexical-binding: t; -*-

;; Copyright (C) 2024  TakesxiSximada

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; URL: https://blog.symdon.info/posts/1708668537/deepl.el
;; Version: 1
;; Package-Requires: ((emacs "28.1") (s))
;; Keywords: translation deepl

;; This file is not part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.

(require 's)

(defcustom deepl-origin "https://api-free.deepl.com"
  "DeepL API Origin"
  :type 'string
  :group 'deeepl)

(defvar deepl-auth-key nil
  "Your DeepL API Key

See: https://www.deepl.com/ja/your-account/summary
")

(defvar deepl-curl-process nil)
(defvar deepl-result-buffer-name "*DeepL result*")
(defvar deepl-process-buffer-name "*DeepL cURL request*")
(defvar deepl-process-error-buffer-name "*DeepL cURL error*")
(defvar deepl-config-buffer-name "*DeepL cURL config*")
(defvar deepl-target-language "en")

(defun deepl-target-language (language)
  (interactive (list (completing-read "Target Language: " '("en" "ja"))))
  (setq deepl-target-language language))

(defun deepl-create-config (&optional source-text)
  (interactive "sDeepL Source Text: ")
  (with-current-buffer (get-buffer-create deepl-config-buffer-name)
    (erase-buffer)
    (insert (format "url %s\n" (json-encode-string (format "%s/v2/translate" deepl-origin))))
    (insert (format "request %s\n" (json-encode-string "POST")))
    (insert (format "header %s\n" (json-encode-string "Content-Type: application/json")))
    (insert (format "header %s\n" (json-encode-string (format "Authorization: DeepL-Auth-Key %s" deepl-auth-key))))
    (insert (format "data %s\n" (json-encode-string (json-encode
						     `((text . (,source-text))
						       (target_lang . ,deepl-target-language))))))))

(defun deepl-send-request ()
  (interactive)

  (with-current-buffer (get-buffer-create deepl-process-buffer-name)
    (erase-buffer))

  (setq deepl-curl-process
	(make-process :name deepl-process-buffer-name
		      :buffer deepl-process-buffer-name
		      :command '("/usr/bin/curl" "-K-")
		      :stderr deepl-process-error-buffer-name
		      :sentinel
		      (lambda (process event)
			(when (not (process-live-p process))
			  (pcase event
			    ("finished\n"
			     (with-current-buffer (get-buffer-create deepl-result-buffer-name)
			       (save-excursion
				 (goto-char (point-max))
				 (mapc #'insert
				       (with-current-buffer (process-buffer process)
					 (goto-char (point-min))
					 (deepl-get-text-list-response (json-read)))))))
			    (t
			     (error "failed to deepl api: event %s" event)))))))

  (with-current-buffer (get-buffer-create deepl-config-buffer-name)
    (process-send-string deepl-curl-process (buffer-string))
    (process-send-eof deepl-curl-process))
  (display-buffer deepl-result-buffer-name))

(defun deepl-get-text-list-response (parsed-json-data)
  (seq-filter (lambda (str) str)
	      (mapcar
	       (lambda (elm)
		 (if-let ((text (alist-get 'text elm)))
		     (format "%s\n" text)))
	       (alist-get 'translations
			  parsed-json-data))))

;;;###autoload
(defun deepl (&optional beg end)
  (interactive "r")
  (deepl-create-config (read-string-from-buffer
			"DeepL Translation"
			(if (region-active-p)
			    (buffer-substring-no-properties beg end)
			  "")))
  (deepl-send-request))

;;;###autoload
(defun deepl-for-assist (content)
  (interactive "r")
  (when content
    (deepl-create-config content)
    (deepl-send-request)))

(provide 'deepl)
;;; deepl.el ends here