Expanding yasnippets by voice in Emacs and other applications

| emacs, audio, speech-recognition

Yasnippet is a template system for Emacs. I want to use it by voice. I'd like to be able to say things like "Okay, define interactive function" and have that expand to a matching snippet in Emacs or other applications. Here's a quick demonstration of expanding simple snippets:

Screencast of expanding snippets by voice in Emacs and in other applications

Transcript
  • 00:00 So I've defined some yasnippets with names that I can say. Here, for example, in this menu, you can see I've got "define interactive function" and "with a buffer that I'll display." And in fundamental mode, I have some other things too. Let's give it a try.
  • 00:19 I press my shortcut. "Okay, define an interactive function." You can see that this is a yasnippet. Tab navigation still works.
  • 00:33 I can say, "OK, with a buffer that I'll display," and it expands that also.
  • 00:45 I can expand snippets in other applications as well, thanks to a global keyboard shortcut.
  • 00:50 Here, for example, I can say, "OK, my email." It inserts my email address.
  • 01:02 Yasnippet definitions can also execute Emacs Lisp. So I can say, "OK, date today," and have that evaluated to the actual date.
  • 01:21 So that's an example of using voice to expand snippets.

This is handled by the following code:

(defun my-whisper-maybe-expand-snippet (text)
  "Add to `whisper-insert-text-at-point'."
  (if (and text
           (string-match
            "^ok\\(?:ay\\)?[,\\.]? \\(.+\\)" text))
    (let* ((name
            (downcase
             (string-trim
              (replace-regexp-in-string "[,\\.]" "" (match-string 1 text)))))
           (matching
            (seq-find (lambda (o)
                        (subed-word-data-compare-normalized-string-distance
                         name
                         (downcase (yas--template-name o))))
                      (yas--all-templates (yas--get-snippet-tables)))))
      (if matching
          (progn
            (if (frame-focus-state)
                (progn
                  (yas-expand-snippet matching)
                  nil)
              ;; In another application
              (with-temp-buffer
                (yas-minor-mode)
                (yas-expand-snippet matching)
                (buffer-string))))
        text))
    text))

This code relies on my fork of whisper.el, which lets me specify a list of functions for whisper-insert-text-at-point. (I haven't asked for upstream review yet because I'm still testing things, and I don't know if it actually works for anyone else yet.) It does approximate matching on the snippet name using a function from subed-word-data.el which just uses string-distance. I could probably duplicate the function in my config, but then I'd have to update it in two places if I come up with more ideas.

The code for inserting into other functions is defined in my-whisper-maybe-type, which is very simple:

(defun my-whisper-maybe-type (text)
  "If Emacs is not the focused app, simulate typing TEXT.
Add this function to `whisper-insert-text-at-point'."
  (when text
    (if (frame-focus-state)
        text
      (make-process :name "xdotool" :command
                    (list "xdotool" "type"
                          text))
      nil)))

Someday I'd like to provide alternative names for snippets. I also want to make it easy to fill in snippet fields by voice. I'd love to be able to answer minibuffer questions from yas-choose-value, yas-completing-read, and other functions by voice too. Could be fun!

Related:

This is part of my Emacs configuration.
View Org source for this post

La semaine du 23 février au premier mar

| french

lundi 23 février

J'ai demandé si ses amis pourraient venir à sa fête demain. Nous avons appris qu'ils étaient malades depuis quelques semaines et ils ne pouvaient pas venir.

J'ai emmené ma fille à son cours de gymnastique à vélo parce que les rues étaient praticables. Elle s'est entraînée à faire la roue. Après ça, nous avons livré des pochettes surprises et des petits gâteaux pour ses amis qui sont malheureusement malades.

Ma fille a voulu faire des biscuits en meringues. Elle a séparé les œufs et les a battus elle-même jusqu'à ce qu'elle soit fatiguée. La première fournée n'a pas marché, mais la deuxième était acceptable. Nous les avons laissés dans le mini-four toute la nuit.

Nous avons découvert que l'axolotl en peluche qui passe au micro-ondes est une façon parfaite de chauffer nos orteils sous les couvertures. Ma fille ne s'habitue pas à l'odeur (c'est probablement la graine de lin avec la lavande), mais si c'est sous les couvertures, ça ne la dérange pas.

mardi 24 février

Les biscuits meringues sont encore trop collants ce matin. Il se trouve que j'ai oublié de les faire cuire au four pendant une heure hier soir. J'ai jeté la moitié de la fournée avant de rechercher une façon de réparer le reste. Heureusement, après les avoir cuits au four pendant une heure à basse température, les biscuits étaient acceptables.

J'ai travaillé sur la prononciation avec mon tuteur. J'ai réessayé les virelangues du rendez-vous précédent, ainsi que de nouveaux :

  • 00:00 Maman peint un grand lapin blanc.
  • 00:04 Un enfant intelligent mange lentement.
  • 00:08 Le roi croit voir trois noix.
  • 00:12 Le témoin voit le chemin loin.
  • 00:16 Moins de foin au loin ce matin.
  • 00:21 La laine beige sèche près du collège.
  • 00:25 La croquette sèche dans l'assiette.
  • 00:28 Elle mène son frère à l'hôtel.
  • 00:31 Le verre vert est très clair.
  • 00:35 Elle aimait manger et rêver.
  • 00:38 Le jeu bleu me plaît peu.
  • 00:41 Ce neveu veut un jeu.
  • 00:44 Le feu bleu est dangereux.
  • 00:47 Le beurre fond dans le cœur chaud.
  • 00:50 Les fleurs de ma sœur sentent bon.
  • 00:54 Le hibou sait où il va.
  • 00:56 L'homme fort mord la pomme.
  • 01:00 Le sombre col tombe.
  • 01:02 L'auto saute au trottoir chaud.
  • 01:07 Le château d'en haut est beau.
  • 01:09 Le cœur seul pleure doucement.
  • 01:14 Tu es sûr du futur.
  • 01:17 Trois très grands trains traversent trois trop grandes rues.
    {tʁwˈa tʁɛ ɡʁˈɑ̃ tʁˈɛ̃ tʁavˈɛʁs tʁwˈa tʁo ɡʁˈɑ̃d ʁˈy.}
  • 01:29 Je veux deux feux bleus, mais la reine préfère la laine beige.
    {ʒə vˈø dˈø fˈø blˈø, mɛ la ʁˈɛn pʁefˈɛʁ la lˈɛn bˈɛʒ.}
  • 01:37 Vincent prend un bain en chantant lentement.
    {vɛ̃sˈɑ̃ pʁˈɑ̃t œ̃ bˈɛ̃ ɑ̃ ʃɑ̃tˈɑ̃ lɑ̃tmˈɑ̃.}
  • 01:44 La mule sûre court plus vite que le loup fou.
    {la mˈyl sˈyːʁ kˈuʁ ply vˈit kə lə lˈu fˈu.}
  • 01:50 Luc a bu du jus sous le pont où coule la boue.
    {lˈyk a bˈy dy ʒˈy su lə pˈɔ̃t u kˈul la bˈu.}

Je n'ai pas enregistré de bonne tentative pour :

  • Le frère de Robert prépare un rare rôti rouge.
    {lə fʁˈɛʁ də ʁobˈɛʁ pʁepˈaʁ œ̃ ʁˈaʁ ʁotˈi ʁˈuʒ.}
  • La mule court autour du mur où hurle le loup.
    {la mˈyl kˈuʁ otˌuʁ dy mˈyʁ u ˈyʁl lə lˈu.}

Si je comprends bien, mon tuteur m'a dit que les sons dans « Maman peint un grand lapin blanc. » sont plus proches les uns des autres que dans la version de la synthèse vocale. Il a aussi prononcé « doucement » avec trois syllabes au lieu de deux. Je me demande si c'est l'accent du Midi. C'est tout à fait acceptable. Maintenant, mon objectif de prononciation est juste d'être compréhensible, pas d'atteindre un accent métropolitain ou canadien. Si j'apprends la prononciation des voyelles nasales et du «r», et que j'apprends les liaisons et les lettres muettes, je pense qu'il me sera facile de prendre un accent acceptable même si ce n'est pas parfait.

Écouter mes enregistrements n'était pas très utile. Il valait mieux lire les virelangues en voix haute pendant le rendez-vous. Peut-être que je dois modifier mon interface pour écouter les courtes parties d'enregistrements. Mais je pense que la préparation des enregistrements avant le rendez-vous a été utile.

J'ai ajouté la fonctionnalité pour couper une partie au milieu de l'enregistrement dans ma bibliothèque compile-media.

Nous avons reporté la fête d'anniversaire de ma fille à cause des maladies de ses amis. Elle a invité deux familles, mais tous les enfants étaient malades. Selon la surveillance des pathogènes dans les eaux usées, quelques maladies sont très fréquentes pour le moment. Nous leur avons donné une pochette surprise et des gâteaux.

Bien que nous n'ayons pas eu de fête, nous avons acheté la pizza que nous avions prévue.

Elle a des crampes, pauvre chérie. L'axolotl réchauffé était une source de réconfort.

mercredi 25 février

Pour une fois, ma fille s'est réveillée à temps pour le petit-déjeuner. Mais l'école virtuelle a une remplaçante aujourd'hui, donc ma fille n'a pas voulu participer à la classe. C'est la vie. Je l'ai laissée décider parce que c'est sa responsabilité.

J'ai soumis le rapport annuel de mon entreprise. C'était simple.

J'ai acheté des fleurs LEGO pour l'anniversaire de ma sœur qui habite aux Pays-Bas. Nous avons les mêmes fleurs et ma fille les adore.

J'ai participé à la réunion virtuelle Emacs Berlin. Quelqu'un nous a demandé comment trier les candidats de saisie, donc j'ai expliqué le mécanisme et j'ai créé un exemple qui trie les candidats différemment. J'ai aussi démontré consult-org-headings et edebug, et j'ai discuté d'Embark et de Consult.

J'ai emmené ma fille à la patinoire pour jouer avec son amie et la troupe de scouts de son amie. J'ai apporté 2 litres de chocolat chaud, qui est plus que suffisant pour tous les enfants. Le père de son amie leur a appris à tourner plus vite. Ils ont aussi joué au loup. Même si quatre filles ont poursuivi le père, elles ne l'ont pas attrapé.

jeudi 26 février

Une fois de plus, ma fille s'est encore réveillée à l'heure du petit-déjeuner. Elle a participé à la classe. Tout allait bien. Après l'école, elle a voulu faire des courses elle-même. Elle a emprunté deux livres à la bibliothèque et elle a acheté quelques collations au supermarché. Je l'ai suivie d'un peu loin pour partager mon Internet. Elle a envie de l'indépendance, mais elle voulait aussi jouer à Pokémon Go.

J'ai modifié le mécanisme de saisie Orderless pour traiter des lettres accentuées. J'ai aussi amélioré mes fonctions qui trient les candidats de saisie par niveau au lieu de par position. Puis j'ai écrit trois articles sur mon blog : deux sur la saisie pour le Carnaval d'Emacs et un sur les intérêts convergents pour le Carnaval IndieWeb. Je suis ravie d'écrire les fonctions et les notes.

En préparation d'un autre article, j'ai rassemblé plus de 300 liens sur la saisie tirés de mon infolettre depuis quelques années. J'ai mis à jour ma fonction pour vérifier les liens et je l'ai utilisée pour identifier les liens morts. J'ai aussi commencé à en catégoriser.

J'ai créé des fonctions pour ma bibliothèque subed-record pour écouter des références audio comme celles que j'avais extraites du rendez-vous avec mon tuteur.

J'ai dû renouveler mes certificats SSL, ce qui a nécessité de mettre à jour mon logiciel pour arrêter et redémarrer le serveur web.

vendredi 27 février

J'ai créé une fonction pour utiliser la synthèse vocale pour générer un fichier de référence audio avec les sous-titres. En la combinant avec les fonctions que j'avais écrites hier, je peux probablement suivre ma progression au fil de plusieurs essais. Je dois penser à une bonne interface pour la comparaison sur Emacs et sur Google Chrome pour faciliter le partage.

Pendant le rendez-vous avec mon tuteur, j'ai encore travaillé sur tous les virelangues. Il a dit que je m'améliorais. Progrès ! Bien sûr, j'ai besoin de plus de travail pour que ce soit plus fluide, particulièrement le « r ». Mais je construis un bon flux de travail pour enregistrer mes tentatives et les réécouter, et j'ai hâte de l'améliorer.

Le soleil brillait l'après-midi. Je me suis assise sur la terrasse de bois et j'ai profité du soleil pendant que j'écrivais mon journal. C'était merveilleux que je puisse me détendre vendredi après-midi. Quand il fait beau, je veux être dehors. Je n'ai fait que taper sur mon smartphone, mais je peux aussi lire sur ma tablette. Regarder des émissions est un peu difficile à cause de la lumière vive. Je pense que ce sera meilleur si je configure finalement une synthèse vocale et Emacspeak sur mon smartphone.

L'article de Christian Tietze m'a fait penser à la façon dont l'éditeur Emacs me permet de faire beaucoup de choses parce qu'il gère bien tous les textes. Il a utilisé Tmux pour capturer l'output et diriger vers l'IA pour fermer la boucle de rétroaction. C'est prometteur.

Après mes rendez-vous avec mon tuteur, j'utilise la reconnaissance vocale pour transcrire l'enregistrement. Maintenant que c'est du texte, je peux utiliser subed.el pour écouter certains moments. Puis je peux utiliser subed-record.el pour extraire des passages dans un fichier audio avec les sous-titres corrigés. Je peux donc les écouter, enregistrer de nouvelles tentatives, et les comparer.

J'ai modifié ma configuration pour la reconnaissance vocale. Maintenant, une fois que je dis « okay, … in French », elle le traduit et affiche le résultat comme une suggestion de saisie au lieu d'insérer directement. Cette façon m'aide à me souvenir.

Ma fille était fatiguée après l'école, donc nous sommes allées jusqu'au parc au lieu de patiner.

samedi 28 février

Les résultats de l'examen médical de ma fille sont arrivés. Son ECG était normal. Elle a dit que ses palpitations sont un peu meilleures. Selon son analyse de sang, son niveau de fer était un peu bas, comme nous tous. Il faut ajuster notre nourriture. Elle me demande si les petits pains aux haricots rouges contiennent du fer. Quelle surprise, ils ont une quantité respectable. Nous sommes tous allés à la pâtisserie chinoise à vélo pour en acheter. En cours de route, nous avons participé aux raids Pokémon et nous avons attrapé quelques mega-Pokémon avec l'aide d'autres dresseurs.

Nous avons fait du lèche-vitrines à IKEA pour réfléchir à des meubles qui conviendraient à notre fille. Elle a envie du lit en mezzanine qui crée un espace pour jouer en dessous. Elle a aussi envie d'une table à abattant avec des étagères. Avant de les acheter, il faut que nous désencombrions sa chambre et que nous mesurions l'espace.

J'ai ajouté des contributions au Carnaval Emacs sur la saisie. J'ai aussi ajouté environ 300 liens issus des archives de l'infolettre Emacs News. C'était une bonne occasion pour apprendre ensemble.

J'ai commencé à regarder les émissions de Pokémon en français sur YouTube. Ma fille adore Pokémon pour le moment, donc si j'en regarde aussi, nous pouvons bavarder. Dans le premier épisode, notre protagoniste Sacha a dormi trop tard et il a reçu le dernier Pokémon, Pikachu, qui n'a pas voulu devenir ami avec lui. Mais une fois que Pikachu a vu comment Sacha a voulu le protéger contre beaucoup de Piafabecs, Pikachu l'a aidé.

J'ai essayé Claude CLI pour générer quelques serveurs MCP pour interroger mon journal en anglais et en français, mes articles sur mon blog, et mes dessins.

dimanche premier mars

J'ai désencombré l'ensemble de tiroirs dans ma chambre et la commode dans la chambre de ma fille. J'ai rempli un sac de choses à donner et j'ai jeté des choses qui étaient cassées ou trop usées.

J'ai relu mon journal pour travailler sur mes dessins quotidiens. Je veux résumer mes revues mensuelles que j'ai perdu l'habitude de faire depuis que j'ai appris le français.

Ma fille a pleuré à cause d'une rage de dents, donc je dois prendre un rendez-vous chez la dentiste bientôt. Elle a dit que ses dents sont trop serrées. Peut-être qu'elle a besoin d'un appareil orthodontique. C'est aussi possible que je ne lui aie pas assez bien brossé les dents. Je vais essayer de faire mieux, et elle doit aussi apprendre à prendre soin d'elle-même.

L'après-midi, ma fille et moi sommes allées au parc pour jouer à Pokémon Go. Nous avons raté l'événement avec des cadeaux, mais nous avons réussi à attraper deux Pokémon légendaires avec l'aide de plusieurs autres dresseurs. Il faisait froid, donc nous sommes rentrées après une heure.

Mon mari a essayé les kits électroniques micro:bit que j'avais achetés pour apprendre l'électronique avec notre fille. Il était un peu frustré par Bluetooth, mais il a finalement réussi avec un câble direct. Je veux toujours bricoler avec le kit, mais je veux aussi apprendre beaucoup d'autres choses. On va voir.

Prononciation

  • 00:00 … les rues étaient praticables
  • 00:03 Elle s'est entraînée à faire la roue.
  • 00:07 Ma fille a voulu faire des biscuits en meringues.
View Org source for this post

Emacs Lisp: defvar-keymap hints for which-key

| emacs

Emacs has far too many keyboard shortcuts for me to remember, so I use which-key to show me a menu if I pause for too long and which-key-posframe to put it somewhere close to my cursor.

(use-package which-key :init (which-key-mode 1))
(use-package which-key-posframe :init (which-key-posframe-mode 1))

I've used which-key-replacement-alist to rewrite the function names and re-sort the order to make them a little easier to scan, but that doesn't cover the case where you've defined an anonymous function ((lambda ...)) for those quick one-off commands. It just displays "function".

Pedro A. Aranda Gutiérrez wanted to share this tip about defining hints by using cons. Here's his example:

(defun insert-surround (opening &optional closing)
 "Insert OPENING and CLOSING and place the cursor before CLOSING.

Default CLOSING is \"}\"."
  (insert opening)
  (save-excursion
    (insert (or closing "}"))))

(defvar-keymap tex-format-map
  :doc "My keymap for text formatting"
  "-"  (cons "under" (lambda() (interactive) (insert-surround
"\\underline{")))
  "b"  (cons "bold"  (lambda() (interactive) (insert-surround "\\textbf{")))
  "e"  (cons "emph"  (lambda() (interactive) (insert-surround "\\emph{")))
  "i"  (cons "ital"  (lambda() (interactive) (insert-surround "\\textit{")))
  "m"  (cons "math"  (lambda() (interactive) (insert-surround "$" "$")))
  "s"  (cons "sans"  (lambda() (interactive) (insert-surround "\\textsf{")))
  "t"  (cons "tty" (lambda() (interactive) (insert-surround "\\texttt{")))
  "v"  (cons "Verb"  (lambda() (interactive) (insert-surround "\\Verb{")))
  "S"  (cons "small" (lambda() (interactive) (insert-surround "\\small{"))))
(fset 'tex-format-map tex-format-map)

Let's try it out:

(with-eval-after-load 'tex-mode
  (keymap-set tex-mode-map "C-c t" 'tex-format-map))
2026-03-02_14-45-51.png
Figure 1: Screenshot with hints

This works for named functions as well. Here's how I've updated my config:

(defvar-keymap my-french-map
  "l" (cons "🔍 lookup" #'my-french-lexique-complete-word)
  "w" (cons "📚 wordref" #'my-french-wordreference-lookup)
  "c" (cons "✏️ conj" #'my-french-conjugate)
  "f" (cons "🇫🇷 fr" #'my-french-consult-en-fr)
  "s" (cons "🗨️ say" #'my-french-say-word-at-point)
  "t" (cons "🇬🇧 en" #'my-french-translate-dwim))
(fset 'my-french-map my-french-map)

(with-eval-after-load 'org
  (keymap-set org-mode-map "C-," 'my-french-map)
  (keymap-set org-mode-map "C-c u" 'my-french-map))
2026-03-02_13-57-23.png
Figure 2: Before: Without the cons, which-key uses the full function name
2026-03-02_14-42-42.png
Figure 3: After: Might be easier to skim?

In case you're adding to an existing keymap, you can use keymap-set with cons.

(keymap-set my-french-map "S" (cons "sentence" #'my-french-say-sentence-at-point))

This is also different from the :hints that show up in the minibuffer when you have a repeating map. Those are defined like this:

(defvar-keymap my-french-map
  :repeat (:hints ((my-french-lexique-complete-word . "lookup")
                   (my-french-consult-en-fr . "fr")
                   (my-french-translate-dwim . "en")
                   (my-french-say-word-at-point . "say")))
  "l" (cons "🔍 lookup" #'my-french-lexique-complete-word)
  "w" (cons "📚 wordref" #'my-french-wordreference-lookup)
  "c" (cons "✏️ conj" #'my-french-conjugate)
  "f" (cons "🇫🇷 fr" #'my-french-consult-en-fr)
  "s" (cons "🗨️ say" #'my-french-say-word-at-point)
  "t" (cons "🇬🇧 en" #'my-french-translate-dwim))

and those appear in the minibuffer like this:

2026-03-02_13-59-42.png
Figure 4: Minibuffer repeat hints

Menus in Emacs are also keymaps, but the labels work differently. These ones are defined with easymenu.

(with-eval-after-load 'org
(define-key-after
 org-mode-map
 [menu-bar french-menu]
 (cons "French"
       (easy-menu-create-menu
        "French"
        '(["🕮Grammalecte" my-flycheck-grammalecte-setup t]
          ["✓Gptel" my-lang-gptel-flycheck-setup t]
          ["🎤Subed-record" my-french-prepare-subed-record t])))
 'org))

Using your own hints is like leaving little breadcrumbs for yourself.

Thanks to Pedro for the tip!

View Org source for this post

2026-03-02 Emacs news

| emacs, emacs-news

Hello folks! Last month's Emacs Carnival about completion had 17 posts (nice!), and Philip Kaludercic is hosting this month's Emacs Carnival: Mistakes and Misconceptions. Looking forward to reading your thoughts!

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View Org source for this post

Emacs Carnival Feb 2026 wrap-up: Completion

| emacs

Check out all the wonderful entries people sent in for the Emacs Carnival Feb 2026 theme of Completion:

Also, this one about completing the loop:

Sometimes I miss things, so if you wrote something and you don't see it here, please let me know! Please e-mail me at sacha@sachachua.com or DM me via Mastodon with a link to your post(s). If you like the idea but didn't get something together in time for February, it's never too late. Even if you come across this years later, feel free to write about the topic if it inspires you. I'd love to include a link to your notes in Emacs News.

I added a ton of links from the Emacs News archives to the Resources and Ideas section, so check those out too.

I had a lot of fun learning together with everyone. I already have a couple of ideas for March's Emacs Carnival theme of Mistakes and Misconceptions (thanks to Philip Kaludercic for hosting!), and I can't wait to see what people will come up with next!

View Org source for this post

Using speech recognition for on-the-fly translations in Emacs and faking in-buffer completion for the results

| audio, speech-recognition, emacs, speech

When 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.":

Screencast of using speech recognition to translate into French and provide a hint when typing

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 ? =)

This is part of my Emacs configuration.
View Org source for this post

Emacs completion and handling accented characters with orderless

| emacs

I like using the orderless completion package for Emacs because it allows me to specify different parts of a completion candidate than any order I want. Because I'm learning French, I want commands like consult-line (which uses minibuffer completion) and completion-at-point (which uses in-buffer completion) to also match candidates where the words might have accented characters. For example, instead of having to type "utilisé" with the accented é, I want to type "utilise" and have it match both "utilise" and "utilisé".

(defvar my-orderless-accent-replacements
  '(("a" . "[aàáâãäå]")
    ("e" . "[eèéêë]")
    ("i" . "[iìíîï]")
    ("o" . "[oòóôõöœ]")
    ("u" . "[uùúûü]")
    ("c" . "[cç]")
    ("n" . "[nñ]"))) ; in case anyone needs ñ for Spanish

(defun my-orderless-accent-dispatch (pattern &rest _)
  (seq-reduce
   (lambda (prev val)
     (replace-regexp-in-string (car val) (cdr val) prev))
   my-orderless-accent-replacements
   pattern))

(use-package orderless
  :custom
  (completion-styles '(orderless basic))
  (completion-category-overrides '((file (styles basic partial-completion))))
  (orderless-style-dispatchers '(my-orderless-accent-dispatch orderless-affix-dispatch)))
2026-02-26_15-06-59.png
Figure 1: Screenshot of consult-line showing matching against accented characters
2026-02-26_15-08-34.png
Figure 2: Screenshot of completion-at-point matching "fev" with "février"

This is an entry for Emacs Carnival February 2026: Completion.

This is part of my Emacs configuration.
View Org source for this post