# go-translate **Repository Path**: lorniu/go-translate ## Basic Information - **Project Name**: go-translate - **Description**: Translator on Emacs. Supports multiple engines such as Google, Bing, deepL, ChatGPT, StarDict, Youdao and so on. - **Primary Language**: Emacs Lisp - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-02-17 - **Last Updated**: 2025-10-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README * gt.el 这是一个翻译框架,其主要特点是高度可配置性和高度可扩展性。 - [[#基本使用][基本使用]] | [[#gt-http-backend][配置网络 (代理和后端)]] - [[#更多配置][了解组件 → 使用组件 → 扩展组件]] + ([[#用于捕获输入的-gt-taker-组件][Taker]]) [[#gt-taker][gt-taker]] + ([[#用于翻译转换的-gt-engine-组件][Engine]]) [[#gt-deepl-engine][gt-deepl-engine]] | [[#gt-stardict-engine][gt-stardict-engine]] | [[#gt-chatgpt-engine][gt-chatgpt-engine]] + ([[#用于渲染输出的-gt-render-组件][Render]]) [[#gt-buffer-render][gt-buffer-render]] | [[#gt-posframe-pop-rendergt-posframe-pin-render][gt-posframe-render]] | [[#gt-insert-render][gt-insert-render]] | [[#gt-overlay-render][gt-overlay-render]] + [[#gt-text-utility][gt-text-utility]] | [[#gt-valid-trait-if][gt-valid-trait (:if 的语法)]] - [[#gt-chatgpt-engine-1][与大模型的交互: 进击的 gt-chatgpt-engine]] - [[#为-emacs-集成-tts-功能][为 Emacs 集成 TTS (语音播报) 功能]] - [[#定制与扩展][扩展组件/框架的基本逻辑和方法]] 作为一个翻译框架,它有很多优点: - 支持多种翻译引擎,比如 Bing, Google, DeepL, 有道词典/近义词, StarDict, LibreTranslate,也支持 LLM 引擎比如 ChatGPT, DeepSeek 等 - 丰富的渲染方式,可以渲染到 Buffer, Posframe, Overlay, Kill Ring 等。支持流式渲染 - 可以灵活获取需要翻译的内容和语言,内置的 Taker 有丰富的选项适配不同场景 - 支持单词翻译,也支持句子翻译,并支持多段落翻译。可同时使用多个引擎,将多个段落,翻译为多种语言 - 支持不同的 HTTP 后端 (url.el, curl),请求纯异步和无阻塞,体验良好 - 基于 eieio (CLOS) 实现,各种组件,允许用户灵活搭配、自由扩展 当然,这不仅是一个翻译框架。它非常灵活,可以轻松扩展到任意文本转换的场合 (Text to Text): - 比如内置的 Text Utility,集成了文本的加密解密、求取哈希、生成二维码等功能 - 比如可以扩展为 ChatGPT 的客户端 (TODO) ** 基本使用 首先,需要通过 MELPA 或者其他途径下载并加载本包。 对于最基本的使用,添加如下代码到配置文件: #+begin_src emacs-lisp (setq gt-langs '(en zh)) (setq gt-default-translator (gt-translator :engines (gt-youdao-dict-engine))) ;; 上述配置的意思是,初始化默认翻译器,让其在 en 和 zh 之间通过有道词典进行翻译, ;; 结果将显示在 Echo Area 中 #+end_src 然后选中某段文本,执行命令 =gt-translate= 即可。 当然,也可以为翻译器指定更多选项,比如: #+begin_src emacs-lisp (setq gt-default-translator (gt-translator :taker (gt-taker :text 'buffer :pick 'paragraph) ; 配置拾取器 :engines (list (gt-bing-engine) (gt-youdao-dict-engine)) ; 指定多引擎 :render (gt-buffer-render))) ; 配置渲染器 ;; 上述配置的意思是,初始化默认翻译器,让其将当前 buffer 中的所有段落交给 Bing 和有道进行翻译, ;; 并将结果通过一个新的 Buffer 渲染出来 (setq gt-default-translator (gt-translator :taker (gt-taker :langs '(en zh) :text 'word :prompt t) :engines (list (gt-stardict-engine :exact t) (gt-youdao-dict-engine) (gt-youdao-suggest-engine)) :render (gt-buffer-render))) ;; 上面配置适用于中英单词互译,初始化翻译器,让其翻译当前或选中单词,使用 StarDict 和有道进行翻译, ;; 结果使用新的 Buffer 渲染出来 #+end_src 除了通过 =gt-default-translator= 设定默认翻译器,也可以通过 =gt-preset-translators= 配置若干预设翻译器: #+begin_src emacs-lisp (setq gt-preset-translators `((ts-1 . ,(gt-translator :taker (gt-taker :langs '(en zh) :text 'word) :engines (gt-bing-engine) :render (gt-overlay-render))) (ts-2 . ,(gt-translator :taker (gt-taker :langs '(en fr ru) :text 'sentence) :engines (gt-google-engine) :render (gt-insert-render))) (ts-3 . ,(gt-translator :taker (gt-taker :langs '(en zh) :text 'buffer :pick 'word :pick-pred (lambda (w) (length> w 6))) :engines (gt-google-engine) :render (gt-overlay-render :type 'help-echo))))) #+end_src 上面预设了三个翻译器: - *ts-1*: 使用 Bing 在 en 和 zh 间进行翻译,翻译的是光标附近的单词或选中的文本,结果将以 overlay 的方式显示在当前位置 - *ts-2*: 使用 Google 在 en, fr 和 ru 间进行翻译,翻译的是光标附近的句子或选中的文本,结果将插入当前 buffer - *ts-3*: 使用 Google 翻译 buffer 中所有长度大于 6 的单词,将鼠标放到被翻译的单词上后,翻译结果将以 popup 方式显示 如果 =gt-default-translator= 为 nil,那么 =gt-preset-translators= 中的第一个将会被作为默认翻译器。 因此就可以通过命令 =gt-translate= 进行翻译,并可通过 =gt-setup= 在不同翻译器间进行切换。 请通过 =M-x customize-group gt= 查看更多配置选项,通过阅读之后的章节了解更多配置细节。 ** 更多配置 翻译框架最核心的组件是翻译器,即 =gt-translator=, 它主要包含如下组件: - =gt-taker= 组件: 用来捕获用户的输入,包括文本和要翻译的语言 - =gt-engine= 组件: 用于将 taker 捕获的内容翻译成对应的目标文本 - =gt-render= 组件: 用于将 engine 翻译的结果聚合起来,并输出到用户 翻译的基本流程 =[输入] -> [翻译/转换] -> [输出]=, 分别对应以上 =[Taker] -> [Engine] -> [Render]= 三种组件。 对翻译器执行 =gt-start= 方法,就会按照基本流程完成一次完整的翻译。 因此,配置的实质就是创建一个翻译器实例,并按需求为其指定不同的组件: #+begin_src emacs-lisp ;; 通过 :taker :engines 和 :render 指定组件; 通过 gt-start 方法执行翻译 (gt-start (gt-translator :taker ... :engines ... :render ...)) ;; gt-translate 命令使用 gt-default-translator 指向的翻译器执行翻译任务 (setq gt-default-translator (gt-translator :taker ... :engines ... :render ..)) (call-interactively #'gt-translate) #+end_src 因此,完善配置之前,需要对组件进行进一步了解。 *** 用于捕获输入的 =gt-taker= 组件 | slot | 介绍 | 值 | |-----------+--------------------------+------------------------------------------------------------------------------| | text | 初始文本 | 字符串或返回字符串的一个函数,也可以是 'buffer 'word 'paragraph 'sentence 等 symbol | | langs | 要翻译的语言 | 列表,比如 '(en zh), '(en ru it fr),如果为空,则采用变量 gt-langs 的值 | | prompt | 交互式确认 | 如果为 t 则通过 Minibuffer 确认,如果为 'buffer 则通过打开一个新 buffer 进行确认 | | pick | 从文本中挑选段落、句子或单词 | 进行挑选的函数,或者 'word 'paragraph 'sentence 等 symbol | | pick-pred | 用于过滤 pick 到的文字 | 传入字符串,输出布尔类型 | | then | take 之后要执行的逻辑,钩子 | 一个以当前 translator 为参数的函数。可以对 take 到的内容进行最后一步修改 | | if | 过滤 | 函数或字面量表达式,用于根据输入的内容决定 taker 是否适用于当前翻译任务 | 当前只内置了一个 Taker 实现,它可以适用大多数场景: : 通过 text 决定初始文本,通过 langs 判定翻译语言,通过 prompt 进行确认,通过 pick 从中摘取某些段落、句子或单词 如果没有为翻译器指定 Taker 或指定了 Taker 但缺乏选项,将使用下面变量的值作为默认选项: #+begin_src emacs-lisp (setq gt-langs '(en zh)) ; 默认的翻译语言,至少要指定两个语言 (setq gt-taker-text 'word) ; 默认情况下,初始文本是光标下的单词。如果有文本选中,优先使用选中文本 (setq gt-taker-pick 'paragraph) ; 默认情况下,会按照段落标准分割初始文本。如果不想使用多段翻译,将其设置为 nil (setq gt-taker-prompt nil) ; 默认情况下,没有 prompt 步骤。如果需要,将其设置为 t 或 'buffer #+end_src 使用 =:taker= 显式为翻译器指定 Taker。比如,下面创建的 Taker 跟上述的配置是一致的: #+begin_src emacs-lisp (gt-translator :taker (gt-taker)) (gt-translator :taker (gt-taker :langs '(en zh) :text 'word :pick 'paragraph :prompt nil)) (gt-translator :taker (lambda () (gt-taker))) ; 可以是一个函数 ;; 也可以是一个 taker 列表,那么返回第一个可用的 ;; 结合 :if 判定,如果 taker 没有 :if 则一定是可用的 ;; 比如下面的例子: 如果有文字被选中,prompt; 如果 buffer 只读,翻译当前段落; 否则翻译当前行 (gt-translator :taker (list (gt-taker :prompt t :if 'selection) (gt-taker :text 'paragraph :if 'read-only) (gt-taker :text 'line))) #+end_src Taker 将使用 =text= 决定初始翻译内容。如果当前有文本被选中,则选中的文本被采用。否则使用下面规则: #+begin_src emacs-lisp ;; 如果是 symbol 使用 thing-at-thing 的逻辑决定初始文本 (gt-translator :taker (gt-taker :text 'word)) ; 当前单词 (默认值) (gt-translator :taker (gt-taker :text 'buffer)) ; 当前 buffer 的内容 (gt-translator :taker (gt-taker :text 'paragraph)) ; 当前段落的内容 (gt-translator :taker (gt-taker :text t)) ; 交互式选中一个 symbol,之后根据 symbol 选取 ;; 如果是一个字符串或返回字符串的函数,则以其作为初始文本 (gt-translator :taker (gt-taker :text "hello world")) ; 固定文本 (gt-translator :taker (gt-taker :text (lambda () (buffer-substring 10 15)))) ; 函数返回值,字符串 (gt-translator :taker (gt-taker :text (lambda () '((10 . 15))))) ; 函数返回值,bounds #+end_src Taker 将从 =langs= 中选取要翻译的语言。默认会结合 =gt-lang-rules= 里的规则进行判定和选取: #+begin_src emacs-lisp (gt-translator :taker (gt-taker :langs '(en zh))) ; 在中、英之间进行翻译 (gt-translator :taker (gt-taker :langs '(en zh ru))) ; 在中、英、俄之间进行翻译 (setq gt-polyglot-p t) ; 如果将此变量设置为 t,那么将进行多语言翻译,即一次翻译成多语言并聚合输出 #+end_src 通过设定 =prompt= 让用户对初始文本和翻译语言进行交互式修改和确认: #+begin_src emacs-lisp ;; 通过 Minibuffer 的方式进行确认。集成了一些快捷键,不仅可以修改文本,也可以切换语言 (gt-translator :taker (gt-taker :prompt t)) ;; 通过打开新 Buffer 的方式进行确认。在某些场合,通过新 Buffer 进行某些调整工作是更合适的 (gt-translator :taker (gt-taker :prompt 'buffer)) #+end_src 最后,会根据 =pick= 和 =pick-pred= 对初始文本进行切割和提取。它返回的内容才是最终要被翻译的: #+begin_src emacs-lisp ;; pick 可以是类似于 text 的 symbol (gt-translator :taker (gt-taker ; 翻译整个 buffer 中所有段落 :text 'buffer :pick 'paragraph)) (gt-translator :taker (gt-taker ; 翻译当前段落中长度大于 6 的单词 :text 'paragraph :pick 'word :pick-pred (lambda (w) (length> w 6)))) ;; pick 也可以是一个函数。下面例子等同于上面,翻译当前段落中长度大于 6 的单词 ;; 也可以实现更复杂、更智能的选取逻辑。比如,只选取生词进行翻译 (defun my-get-words-length>-6 (text) (cl-remove-if-not (lambda (bd) (> (- (cdr bd) (car bd)) 6)) (gt-pick-items-by-thing text 'word))) (gt-translator :taker (gt-taker :text 'paragraph :pick #'my-get-words-length>-6)) ;; 使用 :pick 'fresh-word 实现了“透析翻译”的效果,即结合训练的结果,只显示生僻词 ;; 结合 gt-record-words-as-known/unkown 命令将单词标记(训练)为已经掌握的单词 (gt-translator :taker (gt-taker :text 'paragraph :pick 'fresh-word)) #+end_src *** 用于翻译转换的 =gt-engine= 组件 | slot | 介绍 | 值 | |---------+---------------------------+------------------------------------------------------------------| | parse | 指定解析器 | 解析器或函数 | | cache | 配置缓存 | 如果设为 nil 则为当前 engine 禁用缓存。也可以为不同 engine 指定缓存策略 | | stream | 是否开启 stream 模式 | 只有引擎支持流式 API 这个设置才有用。比如,ChatGPT 引擎 | | delimit | 分隔符 | 如果不为空,则采取「连接-翻译-分割」的翻译策略 | | then | engine 完成后执行的逻辑,钩子 | 一个以当前 task 为参数的函数。可以用于在渲染之前对返回的内容进行最后一步修改 | | if | 过滤 | 函数或字面量表达式,用于根据输入的内容决定当前 engine 是否参与当前翻译任务 | 内置的 Engine 实现有: - =gt-deepl-engine=, DeepL 翻译 - =gt-bing-engine=, 微软翻译 - =gt-google-engine/gt-google-rpc-engine=, Google 翻译 - =gt-chatgpt-engine=, 使用 ChatGPT 进行翻译 - =gt-youdao-dict-engine/gt-youdao-suggest-engine=, 有道词典/有道近义词。主要用于中英互译 - =gt-stardict-engine=, StarDict,支持外挂字典,可以用于离线翻译 - =gt-libre-engine=, LibreTranslate, 可以使用网络服务,也可以搭建本地服务 - =gt-osxdict-engine=, 借助命令 osx-dictionary 调用苹果系统内置的翻译 通过 =:engines= 为翻译器指定引擎。一个翻译器可以有一个或多个引擎,也可以指定一个返回引擎列表的函数: #+begin_src emacs-lisp (gt-translator :engines (gt-google-engine)) (gt-translator :engines (list (gt-google-engine) (gt-deepl-engine) (gt-chatgpt-engine))) (gt-translator :engines (lambda () (gt-google-engine))) #+end_src 若引擎存在多个解析器,则可以通过 =parse= 指定某个从而实现特定解析,比如: #+begin_src emacs-lisp (gt-translator :engines (list (gt-google-engine :parse (gt-google-parser)) ; 详细结果 (gt-google-engine :parse (gt-google-summary-parser)))) ; 简约结果 #+end_src 可以通过 =if= 为不同引擎指定不同翻译任务,比如: #+begin_src emacs-lisp (gt-translator :engines (list (gt-google-engine :if 'word) ; 只有当翻译内容为单词时启用 (gt-bing-engine :if '(and not-word parts)) ; 只有翻译内容不是单词且是多个段落时启用 (gt-deepl-engine :if 'not-word :cache nil) ; 只有翻译内容不是单词时启用; 不缓存 (gt-youdao-dict-engine :if '(or src:zh tgt:zh)) ; 只有翻译中文时启用 (gt-youdao-suggest-engine :if '(and word src:en)))) ; 只有翻译英文单词时启用 #+end_src 可以通过 =cache= 为不同引擎指定不同的缓存策略: #+begin_src emacs-lisp (gt-translator :engines (list (gt-youdao-dict-engine) ; 默认缓存机制 (gt-google-engine :cache nil) ; 禁用缓存 (gt-bing-engine :cache 'word))) ; 只缓存单词 #+end_src #+begin_quote *注意:* 如果 delimit 不为 nil,那么多段落或多单词翻译将采取下面策略: 1. 先将翻译的内容连成一个字符串, 2. 通过一次翻译得到结果, 3. 之后再将结果分割开来的翻译策略。 这时传递给 Engine 翻译的文本是一个单独的字符串。 如果 delimit 为 nil 那么传递给 Engine 的将是一个字符串列表,这时将需要 Engine 有处理列表的能力。 #+end_quote *** 用于渲染输出的 =gt-render= 组件 | slot | 介绍 | 值 | |--------+------------------------+--------------------------------------------------------------------| | prefix | 定制输出中的 Prefix 内容 | 函数或字符串。定制 Prefix 显示格式。Prefix 是输出结果中的语言、引擎提示的文本 | | then | 渲染完成后执行的逻辑,钩子 | 函数或另一个渲染器。可以将渲染任务传递给下一个渲染器,实现多渲染器输出的效果 | | if | 过滤 | 函数或字面量表达式,用于根据输入的内容决定 render 是否适用于当前翻译任务 | 内置的 Render 实现有: - =gt-render=, 默认实现,会将结果输出到 Echo Area - =gt-buffer-render=, 打开一个在新 Buffer 来渲染结果 (*推荐使用*) - =gt-posframe-pop-render=, 在当前位置打开一个 childframe 弹窗来渲染结果 - =gt-posframe-pin-render=, 使用屏幕固定位置的 childframe 窗口来渲染结果 - =gt-insert-render=, 将翻译结果插入到当前 buffer,可设定插入的位置、样式等 - =gt-overlay-render=, 将翻译结果通过 Overlay 的方式进行显示,可设定显示的位置、样式等 - =gt-kill-ring-render=, 将翻译结果保存到 Kill Ring 中 - =gt-alert-render=, 借助 [[https://github.com/jwiegley/alert][alert]] 包将结果显示为系统消息 通过 =:render= 为翻译器配置渲染器。可以通过 =:then= 将多个渲染器串起来搭配使用: #+begin_src emacs-lisp (gt-translator :render (gt-alert-render)) (gt-translator :render (gt-alert-render :then (gt-kill-ring-render))) ; 以系统消息方式展示,并保存进 kill-ring (gt-translator :render (lambda () (if buffer-read-only (gt-buffer-render) (gt-insert-render)))) ; 可以指定函数 #+end_src 可以结合 =:if= 选择使用列表中的第一个可用 render。这可以灵活配置不同情况下 render 的使用。比如: #+begin_src emacs-lisp (gt-translator :render (list (gt-overlay-render :if 'selection) ; 如果翻译的是选中的文字,那么通过 overlay 方式渲染 (gt-posframe-pop-render :if 'word) ; 如果翻译的是单词,那么通过 posframe 方式渲染 (gt-alert-render :if '(and read-only not-word)) ; 如果翻译的是只读 buffer 中的非单词,那么通过 alert 渲染 (gt-buffer-render))) ; 默认,使用新 buffer 进行渲染 #+end_src ** 常用组件/补充说明 *** gt-http-backend 网络请求功能是借助 [[https://github.com/lorniu/pdd.el][pdd.el]] 包实现的。pdd 默认使用内置的 =url.el= 进行请求。 当然也可以使用 =curl= 的方式,需要在系统中安装 curl 和 [[https://github.com/alphapapa/plz.el][plz]] 包,然后对 gt-http-backend 进行指定: #+begin_src emacs-lisp ;; 显式指定使用的客户端 (setq gt-http-backend (pdd-url-backend)) ; 基于 url.el 的 (默认) (setq gt-http-backend (pdd-curl-backend)) ; 基于 curl based 的 ;; 显式指定使用代理的客户端 (setq gt-http-backend (pdd-url-backend :proxy "socks5://127.0.0.1:1080")) ;; 单独指定客户端和代理 (setq gt-http-backend (pdd-curl-backend)) (setq gt-http-proxy "socks5://127.0.0.1:1080") ;; 当然,你可以配置为动态决定使用什么客户端,走什么代理。比如: (setq gt-http-proxy (lambda (request) ; 配置只有符合条件的网站走代理,其他的不走 (when (string-match-p "\\(google.*\\|deepl\\|openai\\)\\.com" (oref request url)) "socks5://127.0.0.1:1080"))) #+end_src 想要了解更多,阅读 [[https://github.com/lorniu/pdd.el][pdd]] 包的文档。 *** gt-taker 如果通过 minibuffer 进行 prompt,那么在 minibuffer 中存在如下快捷键: - =C-n= 和 =C-p= 切换语言 - =C-l= 清空输入 - =C-g= 取消翻译 如果通过 buffer 进行 prompt,那么在打开的 buffer 中默认存在如下快捷键: - =C-c C-c= 提交修改,进行翻译 - =C-c C-k= 取消翻译 - 也可以切换语言、切换组件,通过 mode-line 获取更多信息 可以通过 pick 的 =fresh-word= 选项实现只翻译生僻词的目的。基本步骤: 1. 配置要使用的翻译器,将 pick 指定为 fresh-word, 比如: #+begin_src emacs-lisp (setq gt-default-translator (gt-translator :taker (gt-taker :text 'paragraph :pick 'fresh-word) :engines (gt-bing-engine) :render (gt-overlay-render :sface nil))) #+end_src 2. 对文本进行翻译,默认会对目标文本中的所有单词进行翻译 3. 执行 =gt-record-words-as-known= 命令,按提示将已经掌握的单词记录在案 4. 持续执行 2/3 步骤,训练掌握单词的量。已记录单词作为非生僻词将不会出现在之后的翻译中 5. 可执行 =gt-record-words-as-unknown= 将某单词重新设定为生僻词 6. 本功能有很大的优化提升空间。比如换作用数据库记录,统计单词的翻译次数等,按下不提 *** gt-stardict-engine 这是个支持外挂字典的离线翻译引擎。 首先,需要确保你的系统中已经安装了 [[https://github.com/Dushistov/sdcv][sdcv]]: : sudo pacman -S sdcv 另外,需要下载字典文件放入到相关目录。比如下面是在 Linux 下安装朗道字典文件的示例: #+begin_src shell mkdir -p ~/.stardict/dic cd ~/.stardict/dic wget http://download.huzheng.org/zh_CN/stardict-langdao-ce-gb-2.4.2.tar.bz2 wget http://download.huzheng.org/zh_CN/stardict-langdao-ce-gb-2.4.2.tar.bz2 tar xvf stardict-langdao-ec-gb-2.4.2.tar.bz2 tar xvf stardict-langdao-ce-gb-2.4.2.tar.bz2 sdcv -l #+end_src 之后,你就可以配置使用此引擎了: #+begin_src emacs-lisp ;; 基本配置 (setq gt-default-translator (gt-translator :engines (gt-stardict-engine) :render (gt-buffer-render))) ;; 可以指定更多选项 (setq gt-default-translator (gt-translator :engines (gt-stardict-engine :dir "~/.stardict/dic" ; 指定数据文件位置 :dict "朗道英汉字典5.0" ; 可以指定具体使用的字典 :exact t) ; do not fuzzy-search, only return exact matches :render (gt-buffer-render))) #+end_src *注意*: 如果是通过 Buffer-Render 等渲染,可以通过点击字典名或错误提示实现字典切换 (快捷键: =C-c C-c=)。 *** gt-deepl-engine DeepL 需要 auth-key 才能正常使用,首先需要通过官网进行获取。 然后,可以通过下列方法对 auth-key 进行设置: 1. 在 engine 定义中直接指定: #+begin_example (gt-translator :engines (gt-deepl-engine :key "***")) #+end_example 2. 将 auth-key 存进系统的 =.authinfo= 文件中: #+begin_example machine api.deepl.com login auth-key password *** #+end_example *** gt-chatgpt-engine 它不仅支持 ChatGPT,也支持兼容 OpenAI API 的其他 AI 模型。只需要配置好 url endpoint 和 API key 即可使用。 首先,必须要配置 API key,可以使用下面任一方式: #+begin_src emacs-lisp (gt-chatgpt-engine :key "YOUR_KEY") ; 引擎定义中指定 (setq gt-chatgpt-key "YOUR-KEY") ; 全局变量中指定 (setenv "OPENAI_API_KEY" "YOUR_KEY") ; 添加到环境变量 ;; 建议添加到 authinfo 文件中,那样会自动读取并使用,更安全可靠 (find-file "~/.authinfo") ; machine api.openai.com login apikey password [YOUR_KEY] #+end_src 其次,按照需要修改 host/model 等: #+begin_src emacs-lisp (setq gt-chatgpt-host "YOUR-HOST") (setq gt-chatgpt-model "gpt-4o-mini") (setq gt-chatgpt-temperature 1) (setq gt-chatgpt-extra-options '((n . 1))) ;; or (gt-chatgpt-engine :host :model :extra-options ..) #+end_src 可以自定义翻译的 prompt。比如: #+begin_src emacs-lisp (setq gt-chatgpt-user-prompt-template (lambda (text lang) (format "将下面文本翻译成 %s,措辞要夸张些:\n\n%s" (alist-get lang gt-lang-codes) text))) ;; or (gt-chatgpt-engine :prompt "将下面文本翻译成 {{lang}},措辞要夸张些:\n\n{{text}}") #+end_src 可以通过设置 =:stream t= 让引擎返回流式 (stream) 结果,即内容一点点返回并输出。下面是例子: #+begin_src emacs-lisp ;; 下面配置了三个引擎,第一个是流式的,另外两个是普通的 ;; 其中 Buffer Render, Posframe Render 和 Insert Render 可以进行流式渲染 (setq gt-default-translator (gt-translator :taker (gt-taker :pick nil) :engines (list (gt-chatgpt-engine :stream t) (gt-chatgpt-engine :stream nil) (gt-google-engine)) :render (gt-buffer-render))) ;; 下面配置,将查询的内容一点点插入到 buffer 当前位置 (setq gt-default-translator (gt-translator :taker (gt-taker :pick nil :prompt t) :engines (gt-chatgpt-engine :stream t) :render (gt-insert-render))) #+end_src 多种不同的 LLMs 模型可以同时配置使用: #+begin_src emacs-lisp (setq gt-default-translator (gt-translator :taker (gt-taker :pick nil :prompt t) :engines (list (gt-chatgpt-engine :model "gpt-4o-mini") (gt-chatgpt-engine :host "https://api.deepseek.com" :path "/chat/completions" :model "deepseek-chat") (gt-chatgpt-engine :host "https://api.deepseek.com" :path "/chat/completions" :model "deepseek-reasoner" :prompt "将下面文本翻译成 {{lang}},措辞要夸张些:\n\n{{text}}" :stream t)) :render (gt-buffer-render))) #+end_src 另外,可以通过 =gt-speak= 尝试其语音播报。 *** gt-chatgpt-engine++ 引擎 =gt-chatgpt-engine= 可以脱离翻译功能使用。基于它,可以定制任何与 LLM 相关的任务。 比如,下面的代码定义了一个 Emacs 命令,用于借助 ChatGPT 润色句子。选中文本,调用命令,润色后的结果将替换原来文字: #+begin_src emacs-lisp (defun my-command-polish-using-ChatGPT () (interactive) (gt-start (gt-translator :engines (gt-chatgpt-engine :cache nil :root "你是一个优秀的作家" :prompt (lambda (text) (read-string "Prompt: " (format "润色句子:\n\n%s" text)))) :render (gt-insert-render :type 'replace)))) #+end_src 比如,下面一个命令,用于将 buffer 中所有内容发送给 LLM,并使用一个新的 buffer 显示修正的结果: #+begin_src emacs-lisp (defun my-fix-code-using-ChatGPT () (interactive) (gt-start (gt-translator :taker (gt-taker :text 'buffer :pick nil) :engines (gt-chatgpt-engine :cache nil :prompt (lambda (text) (concat "分析并修复代码中的错误:\n\n" text))) :render (gt-buffer-render :name "*fixup*")))) #+end_src 下面是一个更实用更通用的命令,对于选中的文本跟指定的模型进行交互。日常使用方便,强烈推荐: #+begin_src emacs-lisp (defvar my-ai-oneshot-prompts (list "优化、润色文本" "请认真分析代码,修复代码存在的错误,并给出建议")) (defvar my-ai-oneshot-models (list "deepseek-chat" "gpt-4o-mini" "gemini-2.5-pro")) (defvar my-ai-oneshot-last-model nil) (defvar my-ai-oneshot-history nil) (defun my-ai-oneshot () "Use C-. C-, to switch model." (interactive) (require 'gt) (let ((prompt nil) (model (or my-ai-oneshot-last-model (setq my-ai-oneshot-last-model (or (car my-ai-oneshot-models) gt-chatgpt-model))))) (cl-flet ((get-prompts () (cl-delete-duplicates (append my-ai-oneshot-history my-ai-oneshot-prompts) :from-end t :test #'equal)) (change-model (&optional prev) (let* ((pos (or (cl-position my-ai-oneshot-last-model my-ai-oneshot-models :test #'equal) -1)) (next (if prev (max 0 (1- pos)) (min (1- (length my-ai-oneshot-models)) (1+ pos))))) (setq my-ai-oneshot-last-model (nth next my-ai-oneshot-models)) (setq model my-ai-oneshot-last-model) (overlay-put (car (overlays-at 1)) 'after-string my-ai-oneshot-last-model)))) (setq prompt (minibuffer-with-setup-hook (lambda () (local-set-key (kbd "C-,") (lambda () (interactive) (change-model))) (local-set-key (kbd "C-.") (lambda () (interactive) (change-model t))) (overlay-put (make-overlay 1 9) 'after-string model) (use-local-map (make-composed-keymap nil (current-local-map)))) (completing-read "Prompt (): " (get-prompts) nil nil nil 'my-ai-oneshot-history))) (gt-start (gt-translator :taker (gt-taker :text 'point :pick nil :prompt (lambda (translator) (let ((text (car (oref translator text)))) (oset translator text (list (if (string-blank-p text) prompt (let ((str (if (string-blank-p prompt) text (format "%s\n\n内容如下:\n\n%s\n" prompt text)))) (if current-prefix-arg (read-string "Ensure: " str) str)))))) (message "Processing..."))) :engines (gt-chatgpt-engine :cache nil :stream t :model model :timeout 300 :prompt #'identity) :render (gt-buffer-render :name (format "*ai-oneshot-%s*" model) :mode 'markdown-mode :init (lambda () (markdown-toggle-markup-hiding 1)) :dislike-header t :dislike-source t :window-config '((display-buffer-below-selected)))))))) #+end_src 上面例子充分利用了框架内置的 taker 和 renderer,并将 LLM 的功能结合其中,流畅丝滑,简洁易用。 你也可以发挥想象,在任何需要用到 LLM 的地方,通过类似方法封装适合自己的命令。 另外,如果你需要的仅仅是 LLM 返回的结果,而不需要 taker 和 renderer 的介入,那可以直接调用更底层的 =gt-chatgpt-send= 获取与 LLM 交互的结果,并将其运用在你自己的程序逻辑中。 比如: #+begin_src emacs-lisp ;; 单次对话 (let* ((rs (gt-chatgpt-send "世界上最大的湖泊是哪个?" :sync t)) (content (let-alist rs (let-alist (aref .choices 0) .message.content)))) (message ">>> %s" content)) ;; 多次对话 (let* ((rs (gt-chatgpt-send '(((role . user) (content . "世界上最大的湖泊是哪个?")) ((role . assistant) (content . "世界上最大的湖泊是里海.")) ((role . user) (content . "我觉得不是。"))) :url "https://api.deepseek.com/chat/completions" :model "deepseek-chat" :sync t)) (content (let-alist rs (let-alist (aref .choices 0) .message.content)))) (message ">>> %s" content)) ;; 异步请求 (默认,非阻塞模式) (pdd-then (gt-chatgpt-send "世界上最大的湖泊是哪个?") (lambda (rs) (message "> %s" (let-alist rs (let-alist (aref .choices 0) .message.content))))) ;; 谁是世界上最大的湖泊?谁说的对? (pdd-async ;; answer by ChatGPT and DeepSeek (let* ((q1 "世界上最大的湖泊是哪个?") (rs (await (gt-chatgpt-send q1 :model "gpt-4o-mini") (gt-chatgpt-send q1 :model "deepseek-chat"))) (c1 (let-alist (car rs) (let-alist (aref .choices 0) .message.content))) (c2 (let-alist (cadr rs) (let-alist (aref .choices 0) .message.content)))) (message ">>> ChatGPT: %s" c1) (message ">>> DeekSeek: %s" c2) (message ">>> Judging by Gemini...") ;; judge by Gemini (let* ((q2 (concat "对于'" q1 "'这个问题,我问了 ChatGPT 和 DeepSeek。" "ChatGPT 的回答是: \n\n" c1 "\n\n" "DeepSeek 的回答是: \n\n" c2 "\n\n" "你怎么评价这两个回答的质量?")) (rn (await (gt-chatgpt-send q2 :model "gemini-2.5-flash"))) (c3 (let-alist rn (let-alist (aref .choices 0) .message.content)))) (message ">>> 我是 Gemini, 这是我的回答:\n\n%s" c3)))) #+end_src 通过这种方式,可以将 LLM 交互功能集成到任意程序逻辑中。这不仅适用于 ChatGPT,也适用于 DeepSeek 等。 *** gt-buffer-render 打开一个新的 buffer 来展示翻译结果。这是非常通用的一种展示结果的方式。 在弹出的 buffer 中,存在若干快捷键 (可以通过 =?= 获取到相关提示),比如: - 通过 =t= 切换语言 - 通过 =T= 切换多语言模式 - 通过 =c= 清除缓存 - 通过 =g= 刷新 - 通过 =q= 退出 另外,通过 =y= (命令 =gt-speak=) 播放语音。可以先选中文本,然后通过 =y= 只播放选取片段的语音。 这需要这些引擎已经实现了语音播放的功能。另外,在其他任何地方调用 =gt-speak= 命令,将会尝试 使用操作系统本身的 TTS 功能对当前的文本进行语音播报。 可以通过 =name/window-config/split-threshold= 等对弹出的窗口进行设定: #+begin_src emacs-lisp (gt-translator :render (gt-buffer-render :name "abc" :window-config '((display-buffer-at-bottom)) :then (lambda () (pop-to-buffer "abc")))) #+end_src 下面是若干使用示例: #+begin_src emacs-lisp ;; 捕获光标下的单词或选区,使用 Google 翻译单词,使用 DeepL 翻译句子,使用 Buffer 展示结果 ;; 这是非常通用的一种配置方式 (setq gt-default-translator (gt-translator :taker (gt-taker :langs '(en zh) :text 'word) :engines (list (gt-google-engine :if 'word) (gt-deepl-engine :if 'not-word)) :render (gt-buffer-render))) ;; 封装了一个命令,用于将 Buffer 中的多个段落翻译为多种语言,并渲染到新的 Buffer 中 ;; 这主要展示了命令的封装,以及多引擎多段落多语言的聚合显示效果 (defun demo-translate-multiple-langs-and-multiple-parts () (interactive) (let ((gt-polyglot-p t) (translator (gt-translator :taker (gt-taker :langs '(en zh ru ja) :text 'buffer :pick 'paragraph) :engines (list (gt-google-engine) (gt-deepl-engine)) :render (gt-buffer-render)))) (gt-start translator))) #+end_src *** gt-posframe-pop-render/gt-posframe-pin-render 需要安装 [[https://github.com/tumashu/posframe][posframe]] 之后才能使用。 这两个 Render 的效果跟 =gt-buffer-render= 类似,只不过它的窗口是浮动的。 快捷键也是一致的,比如 =q= 表示退出。 可以通过 =:frame-params= 向 posframe 传递任意需要的参数: #+begin_src emacs-lisp (gt-posframe-pin-render :frame-params (list :border-width 20 :border-color "red")) #+end_src *** gt-insert-render 将翻译结果插入到当前 buffer。 可以指定如下类型 (=type=): - =after=, 默认类型,将结果插入到光标之后 - =replace=, 用翻译结果替换被翻译的源文本 如果对默认的输出格式和样式不满意,可以通过如下选项进行调整: - =sface=, 翻译完成后,被翻译的源文本的 face - =rfmt=, 翻译结果的输出格式 - =rface=, 为翻译结果指定特定样式 选项 =rfmt= 是一个包含控制字符 =%s= 的字符串,也可以是一个函数: #+begin_src emacs-lisp ;; %s 是翻译结果的占位符 (gt-insert-render :rfmt " [%s]") ;; 一个参数,传入的是翻译结果字符串 (gt-insert-render :rfmt (lambda (res) (concat " [" res "]"))) ;; 两个参数,则第一个是源文本 (gt-insert-render :rfmt (lambda (stext res) (if (length< stext 3) (concat "\n" res) (propertize res 'face 'font-lock-warning-face))) :rface 'font-lock-doc-face) #+end_src 下面是若干使用示例: #+begin_src emacs-lisp ;; 按段落进行翻译,将每一段翻译的结果,插入到段落后面 ;; 这种配置适合文章的翻译工作。基本流程是: 翻译 -> 修改 -> 保存 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'buffer :pick 'paragraph) :engines (gt-google-engine) :render (gt-insert-render :type 'after))) ;; 翻译当前段落,并使用翻译的结果替换掉被翻译的段落 ;; 这种配置适合即时聊天等场合。输入文本,翻译得到译文,执行发送 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'paragraph :pick nil) :engines (gt-google-engine) :render (gt-insert-render :type 'replace))) ;; 将当前段落中符合条件的单词进行翻译,并将结果插入到单词之后 ;; 这种配置方式,可以辅助阅读有生僻字的文章 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'paragraph :pick 'word :pick-pred (lambda (w) (length> w 6))) :engines (gt-google-engine) :render (gt-insert-render :type 'after :rfmt " (%s)" :rface '(:foreground "grey")))) #+end_src *** gt-overlay-render 使用 Overlay 显示翻译结果。 通过 =type= 设置显示的方式: - =after=, 默认类型,将翻译结果显示在源文本后面 - =before=, 将翻译结果显示在源文本前面 - =replace=, 将翻译结果覆盖显示到源文本上面 - =help-echo=, 鼠标移动到源文本上时,翻译结果才弹出显示 它在很多方面跟 =gt-insert-render= 很像,包括选项: - =sface=, 翻译完成后,被翻译的源文本的 face - =rfmt=, 翻译结果的输出格式 - =rface/rdisp=, 为翻译结果指定特定样式 - =pface/pdisp=, 单独为翻译后的 Prefix (语言、引擎的提示) 设定样式 下面是若干使用示例: #+begin_src emacs-lisp ;; 翻译 buffer 中所有段落,将结果通过指定格式显示在原段落之后 ;; 这是一种适合阅读 Info, News 等只读内容的配置 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'buffer :pick 'paragraph) :engines (gt-google-engine) :render (gt-overlay-render :type 'after :sface nil :rface 'font-lock-doc-face))) ;; 将 Buffer 中所有符合条件的单词做标记,当鼠标移上去的时候显示翻译结果 ;; 这是一种实用的配置,适合阅读存在某些生僻词的文章 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'buffer :pick 'word :pick-pred (lambda (w) (length> w 5))) :engines (gt-google-engine) :render (gt-overlay-render :type 'help-echo))) ;; 也可以将符合条件单词的翻译直接显示在原单词后面 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'buffer :pick 'word :pick-pred (lambda (w) (length> w 5))) :engines (gt-google-engine) :render (gt-overlay-render :type 'after :sface nil :rfmt "%s" :rdisp '(space (:width 0.3) raise 0.6) :rface '(:foreground "grey" :height 0.5)))) ;; 使用 Overlay 把翻译的结果直接覆盖到原文之上 ;; 对于某篇文章,如果想通过速览的方式获取其大致意思,适合使用这种配置 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'buffer) :engines (gt-google-engine) :render (gt-overlay-render :type 'replace))) #+end_src *** gt-text-utility 派生自 =gt-translator= 的一个组件,集成了很多文本转换和处理方面的功能。 这展示了本框架的扩展性,它不仅可以应用在翻译方面,其 taker 和 render 具备普适性。 如果要生成二维码,需要在系统中安装 =qrencode= 程序或通过 MELPA 安装 =qrencode= 包: #+begin_src sh pacman -S qrencode brew install qrencode # or in Emacs M-x package-install qrencode #+end_src 另外,可以通过扩展 generic 方法 =gt-text-util= 集成其他想要的功能。 下面是若干使用示例: #+begin_src emacs-lisp ;; 默认情况下,通过 completing-read 选择如何进行文本处理 ;; 注意:无需为其指定 engines (setq gt-default-translator (gt-text-utility :render (gt-buffer-render))) ;; 为当前文本生成二维码 (通过 :langs 指定 utility) ;; 实用的配置,适用于电脑向手机传递文本片段 (setq gt-default-translator (gt-text-utility :taker (gt-taker :langs '(qrcode) :pick nil) :render (gt-buffer-render))) ;; 为 buffer 中的每段文字都生成 TTS 按钮以及其 md5 值 (setq gt-default-translator (gt-text-utility :taker (gt-taker :langs '(speak md5) :text 'buffer :pick 'paragraph) :render (gt-posframe-pin-render))) #+end_src *** gt-valid-trait (:if) 组件 =gt-taker=, =gt-engine= 和 =gt-render= 等都继承了 =gt-valid-trait=, 它通过 =:if= 提供了判定组件可用性的方式, 因此可以大大简化不同场景下 translator 的配置。 槽 =:if= 的值可以是函数,也可以是内置实现的一些 symbol, 或者通过 and/or 连接起来的 form 列表。 另外, symbol 可以使用 =not-= 或 =no-= 为前缀表示反向判定。 部分内置 symbol: - =word= 翻译的文本是单词 - =src:en= 翻译的源语言是英语 - =tgt:en= 翻译的目的语言是英语 - =parts= 翻译的是分段的文本 - =at-word= 光标目前正位于 word/symbol 上面 - =read-only= 当前 buffer 是只读的 - =selection= 当前翻译的是选中的文本 - =emacs-lisp-mode= 以 mode 结尾,表示须匹配当前模式 - =not-word= or =no-word= 反向判定,翻译的文本 *不是* 单词 一个粗糙的配置示例: #+begin_src emacs-lisp ;; 对于选中的文本,不分段,并使用 posframe 渲染 ;; 对于 Info,翻译当前段落,使用 overlay 显示结果 ;; 对于只读文本,翻译整个 buffer 中的生词,并使用 overlay 渲染 ;; 对于 Magit commit buffer,将翻译结果插入到光标位置 ;; 对于单词,使用 google 引擎翻译; 其他使用 deepl 引擎 (setq gt-default-translator (gt-translator :taker (list (gt-taker :pick nil :if 'selection) (gt-taker :text 'paragraph :if '(Info-mode help-mode)) (gt-taker :text 'buffer :pick 'fresh-word :if 'read-only) (gt-taker :text 'word)) :engines (list (gt-google-engine :if 'word) (gt-deepl-engine :if 'no-word)) :render (list (gt-posframe-pop-render :if 'selection) (gt-overlay-render :if 'read-only) (gt-insert-render :if (lambda () (member (buffer-name) '("COMMIT_EDITMSG")))) (gt-alert-render :if '(and xxx-mode (or not-selection (and read-only parts)))) (gt-buffer-render)))) #+end_src ** 为 Emacs 集成 TTS 功能 #+begin_quote 注意: 使用语音播放功能之前,需要安装并配置 MPV 播放器 ([[https://mpv.io]])。 #+end_quote 有些引擎通过 =gt-speech= 方法实现了 TTS (Text to Speech) 功能。使用起来很简单: #+begin_src emacs-lisp (gt-speech (gt-bing-engine) "hello" 'en) (gt-speech (gt-google-engine) "hello" 'en) (gt-speech (gt-chatgpt-engine) "hello" 'en) #+end_src 另外,还集成了一些其他 TTS 实现,方便脱机或其他场景的使用: #+begin_src emacs-lisp ;; 使用 macOS 的 say 进行播报 (gt-speech 'mac-say "hello" nil) (setq gt-tts-mac-say-voice "Shelley") ; 可以对 speed 和 voice 进行设置 ;; 使用 Windows 自带的功能进行播报 (gt-speech 'win-ps1 "hello" nil) (setq gt-tts-win-ps1-speed 1.2) ;; 使用第三方的 edge-tts 进行播报 (pip install edge-tts) (gt-speech 'edge-tts "hello" nil) (gt-tts-edge-tts-change-voice) ; 动态切换 voice #+end_src 此外,还为上述的其他实现提供了一个快捷方式名为 =native=: #+begin_src emacs-lisp ;; 默认情况下 macOS: mac-say, 而 Windows: win-ps1 (gt-speech 'native "hello" 'en) ;; 你可以决定 native 使用哪个实现 (setq gt-tts-native-engine 'edge-tts) #+end_src 用户命令 =gt-speak= 是可以交互性使用上述所有 TTS 实现的一个 *核心命令*: - 在 Emacs 的任意地方,选中文本,执行 =M-x gt-speak=, 语音播报就开始了 - 播报开始前,在弹出的 minibuffer 中,可以通过 =C-n= 切换 TTS 引擎或修改播报的文本 ------------- 最后,提供一个综合利用上述 TTS 函数实现自己功能的完整例子: - https://github.com/lorniu/speak-buffer.el 它用来在 Emacs 内逐段阅读当前 buffer 内容 (就像某些阅读 App 中的那样)。 它是异步而且带缓存的,在阅读文章和小说方面体验不错。非常实用,推荐尝试。 ** 定制与扩展 代码基于 eieio (CLOS) 编写,所有的组件都是类,因此几乎每一部分都是可以扩展或替换的。 比如,要实现一个引擎,让它将捕获的文本倒序输出。实现起来很简单: #+begin_src emacs-lisp ;; 首先,定义引擎,继承自 gt-engine (defclass my-reverse-engine (gt-engine) ((delimit :initform nil))) ;; 其次,为引擎实现 gt-execute 方法 (cl-defmethod gt-execute ((_ my-reverse-engine) task) (cl-loop for c in (oref task text) collect (reverse c))) ;; 最后,配置使用 (setq gt-default-translator (gt-translator :engines (my-reverse-engine))) #+end_src 比如,想扩展 Taker,让它能够捕获 org mode 中所有的标题。也很简单: #+begin_src emacs-lisp ;; [实现] 让 Taker 的 text 支持 org-headline,只需要对方法进行特化 (cl-defmethod gt-thing-at-point ((_ (eql 'org-headline)) (_ (eql 'org-mode))) (let (bds) (org-element-map (org-element-parse-buffer) 'headline (lambda (h) (save-excursion (goto-char (org-element-property :begin h)) (skip-chars-forward "* ") (push (cons (point) (line-end-position)) bds)))))) ;; [使用] 通过 :text org-headline 捕获所有 headline; 通过 overlay 展示结果 (setq gt-default-translator (gt-translator :taker (gt-taker :text 'org-headline) :engines (gt-google-engine) :render (gt-overlay-render :rfmt " (%s)" :sface nil))) #+end_src 如此这般,只要发挥想象,将可以做到很多。 ** 欢迎提供反馈跟建议 要打开调试,需要将 =gt-debug-p= 设为 t。之后将能在 =*gt-log*= 中查看日志内容。 我使用翻译不多,这个框架纯粹是兴趣使然。因为对翻译工作的认知有限,某些功能设置未必合理。 因此若有同学和专业人士提出好的想法和建议,必欣然受之。请不吝赐教,谢谢。