Using speech recognition for on-the-fly translations in Emacs and faking in-buffer completion for the results
| audio, speech-recognition, emacs, speechWhen I'm writing a journal entry in French, I sometimes want to translate a phrase that I can't look up word by word using a dictionary. Instead of switching to a browser, I can use an Emacs function to prompt me for text and either insert or display the translation. The plz library makes HTTP requests slightly neater.
(defun my-french-en-to-fr (text &optional display-only)
(interactive (list (read-string "Text: ") current-prefix-arg))
(let* ((url "https://translation.googleapis.com/language/translate/v2")
(params `(("key" . ,(getenv "GOOGLE_API_KEY"))
("q" . ,text)
("source" . "en")
("target" . "fr")
("format" . "text")))
(query-string (mapconcat
(lambda (pair)
(format "%s=%s"
(url-hexify-string (car pair))
(url-hexify-string (cdr pair))))
params
"&"))
(full-url (concat url "?" query-string)))
(let* ((response (plz 'get full-url :as #'json-read))
(data (alist-get 'data response))
(translations (alist-get 'translations data))
(first-translation (car translations))
(translated-text (alist-get 'translatedText first-translation)))
(when (called-interactively-p 'any)
(if display-only
(message "%s" translated-text)
(insert translated-text)))
translated-text)))
I think it would be even nicer if I could use speech synthesis, so I can keep it a little more separate from my typing thoughts. I want to be able to say "Okay, translate …" or "Okay, … in French" to get a translation. I've been using my fork of natrys/whisper.el for speech recognition in English, and I like it a lot. By adding a function to whisper-after-transcription-hook, I can modify the intermediate results before they're inserted into the buffer.
(defun my-whisper-translate ()
(goto-char (point-min))
(let ((case-fold-search t))
(when (re-search-forward "okay[,\\.]? translate[,\\.]? \\(.+\\)\\|okay[,\\.]? \\(.+?\\) in French" nil t)
(let* ((s (or (match-string 1) (match-string 2)))
(translation (save-match-data (my-french-en-to-fr s))))
(replace-match
(propertize translation
'type-hint translation
'help-echo s))))))
(with-eval-after-load 'whisper
(add-hook 'whisper-after-transcription-hook 'my-whisper-translate 70))
But that's too easy. I want to actually type things myself so that I get more practice. Something like an autocomplete suggestion would be handy as a way of showing me a hint at the cursor. The usual completion-at-point functions are too eager to insert things if there's only one candidate, so we'll just fake it with an overlay. This code works only with my whisper.el fork because it supports using a list of functions for whisper-insert-text-at-point.
(defun my-whisper-maybe-type-with-hints (text)
"Add this function to `whisper-insert-text-at-point'."
(let ((hint (and text (org-find-text-property-in-string 'type-hint text))))
(if hint
(progn
(my-type-with-hint hint)
nil)
text)))
(defvar-local my-practice-overlay nil)
(defvar-local my-practice-target nil)
(defvar-local my-practice-start nil)
(defun my-practice-cleanup ()
"Remove the overlay and stop monitoring."
(when (overlayp my-practice-overlay)
(delete-overlay my-practice-overlay))
(setq my-practice-overlay nil
my-practice-target nil
my-practice-start nil)
(remove-hook 'post-command-hook #'my-practice-monitor t))
(defun my-practice-monitor ()
"Updates hint or cancels."
(let* ((pos (point))
(input (buffer-substring-no-properties my-practice-start pos))
(input-len (length input))
(target-len (length my-practice-target)))
(cond
((or (< pos my-practice-start)
(> pos (+ my-practice-start target-len))
(string-match "[\n\t]" input)
(string= input my-practice-target))
(my-practice-cleanup))
((string-prefix-p (downcase input) (downcase my-practice-target))
(let ((remaining (substring my-practice-target input-len)))
(move-overlay my-practice-overlay pos pos)
(overlay-put my-practice-overlay 'after-string
(propertize remaining 'face 'shadow))))
(t ; typo
(move-overlay my-practice-overlay pos pos)
(overlay-put my-practice-overlay 'after-string
(propertize (substring my-practice-target input-len) 'face 'error))))))
(defun my-type-with-hint (string)
"Show hints for STRING."
(interactive "sString to practice: ")
(my-practice-cleanup)
(setq-local my-practice-target string)
(setq-local my-practice-start (point))
(setq-local my-practice-overlay (make-overlay (point) (point) nil t t))
(overlay-put my-practice-overlay 'after-string (propertize string 'face 'shadow))
(add-hook 'post-command-hook #'my-practice-monitor nil t))
Here's a demonstration of me saying "Okay, this is a test, in French.":
Since we're faking in-buffer completion here, maybe we can still get away with considering this as an entry for Emacs Carnival February 2026: Completion ? =)