Emacs: This year, I will... / Cette année, je vais...

| emacs
In English

Inspired by the Emacs Carnival theme for January, this year, I will:

  • take more notes, perhaps with the help of speech recognition
    • Now I can manage my time better since the kiddo can study more independently, but I still have to manage interruptions from my life and from my own brain. If I write or dictate notes while I think or work, I can get back into things more easily. Speech recognition allows me to capture more thoughts, even if the results need a fair bit of reviewing and revising. I'm looking forward to checking out the ideas others have shared, such as configuring more transient.el interfaces and adding more tweaks to my Org Mode publishing.
  • stream to think out loud
    • My daily routines are gradually becoming more predictable, so I might be able to schedule streams or jump onto one to share ideas. If I do a live broadcast, I can learn from the questions and comments I get. Fridays at 10:30 AM seems like a good time for that, and when possible, I can also do it spontaneously.
  • record videos to share what I learn, and package my functions to make them reusable when possible
    • Like the previous points, sharing my ideas and my work can help others and start conversations. If I improve subed-record.el to combine video and audio recordings with screenshots and subtitles, I might be able to create videos more easily.
  • write more in French and improve my environment for this purpose
    • I like the mental stimulation of writing in another language. Lots of other people are learning languages too, so we might be able to help each other. For example, my functions for highlighting new words and grammatical errors could be useful.
  • practice speaking by making audio recordings with the help of subed-record and speech synthesis to improve my pronunciation
    • The advantage of implementing this in Emacs is that I can customize my workflow. For example, I want to write my draft and then record the audio sentence by sentence after listening to an example. I also want to see if I can more easily edit a recording of my session with my tutor so that I can keep only my last pronunciation attempts.
  • improve processes for EmacsConf and Emacs News
    • I might be able to organize much of it by myself, but other volunteers might also be able to help, which would be even better. I want to create the infrastructure to manage several virtual meetings simultaneously, probably with speech recognition, audio spatialization, and lots of keyboard shortcuts. I also want to improve my subtitling process.

The more I do in Emacs, the more I think of ideas for improvement…

En français

Sur le thème du Carnaval d'Emacs pour janvier - Cette année, je vais :

  • prendre plus de notes, peut-être avec l'aide de la reconnaissance vocale
    • Même si je peux mieux gérer mon temps maintenant que ma fille peut étudier de manière plus indépendante, je dois toujours gérer les interruptions de ma vie et de mon propre cerveau. Si j'écris ou dicte des notes pendant que je pense ou que je travaille, je reprendrai le fil de mes pensées plus facilement. La reconnaissance vocale me permet de capter plus de pensées, même si les résultats nécessitent de revoir et de réviser. Je suis enthousiasmée par les idées que d'autres partagent, comme la configuration de l'interface transient.el et l'amélioration de la publication d'Org Mode.
  • streamer pour réfléchir à voix haute
    • Mes routines quotidiennes deviennent plus prévisibles petit à petit, je peux donc planifier des événements ou me lancer dans le streaming pour partager des idées. Si je fais une diffusion en direct, je peux profiter des questions et des commentaires que je reçois. Le vendredi à 10h30 semble être un bon moment pour ça, et quand c'est possible, je peux aussi le faire spontanément.
  • enregistrer des vidéos pour partager ce que j'apprends, et packager mes fonctions pour les rendre réutilisables quand c'est possible
    • Comme les éléments précédents, partager mes idées et mon travail peut aider d'autres personnes et lancer des conversations. Si j'améliore subed-record.el pour combiner des enregistrements vidéo et audio avec des captures d'écran et des sous-titres, je peux créer des vidéos plus facilement.
  • écrire davantage en français et améliorer mon environnement à cet effet
    • J'apprécie la stimulation mentale d'écrire dans une autre langue. Beaucoup de gens apprennent d'autres langues, donc nous pouvons nous aider. Par exemple, mes fonctions pour surligner les nouveaux mots et les erreurs grammaticales pourraient être utiles.
  • pratiquer l'expression orale en faisant des enregistrements audio avec l'aide de subed-record et la synthèse vocale pour améliorer ma prononciation
    • L'avantage de l'implémentation dans Emacs est que je peux personnaliser mon flux de travail. Par exemple, je veux écrire mon brouillon, puis enregistrer l'audio phrase par phrase après l'avoir écouté. Je veux aussi modifier un enregistrement de mon rendez-vous avec ma tutrice pour que je garde seulement mes dernières essaies de prononciation.
  • améliorer les processus pour EmacsConf et Emacs News
    • Il se peut que je puisse l'organiser en grande partie par moi-même, mais il se peut aussi que d'autres bénévoles m'aident, ce qui serait encore mieux. Je veux créer l'infrastructure pour gérer plusieurs réunions virtuelles simultanément, probablement grâce à la reconnaissance vocale, la spatialisation audio, et de nombreux raccourcis clavier. Je veux aussi améliorer mon processus de sous-titrage.

Plus j'en fais dans Emacs, plus je pense à des idées d'amélioration…

Thanks to Christian Tietze for hosting!

View org source for this post

2026-01-26 Emacs news

| emacs, emacs-news
View org source for this post

Queuing multiple transcriptions with whisper.el speech recognition

| audio, speech, emacs

I want to be able to talk out loud and have the ideas go into Emacs. I can do this in a number of different ways:

  1. I briefly demonstrated a step-by-step approach with natrys/whisper.el with a single file. I press a keyboard shortcut to start the recording, another shortcut to stop the recording, and it transcribes it in the background. But the way whisper.el is set up is that if I press the keyboard shortcut to start recording again it will offer to interrupt the transcription process, which is not what I want. I want to just keep talking and have it process results as things come in.
  2. I'm also experimenting with Google Chrome's web speech API to do continuous speech recognition, which I can get into Emacs using a web socket.
  3. What I've just figured out is how to layer a semi-continuous interface for speech recognition on top of whisper.el so that while it's processing in the background, I can just press a keyboard shortcut (I'm using numpad 9 to call my-whisper-continue) to stop the previous recording, queue it for processing, and start the next recording. If I use this keyboard shortcut to separate my thoughts, then Whisper has a much easier time making sense of the whole sentence or paragraph or whatever, instead of trying to use the sliding 30 second context window that many streaming approaches to speech recognition try to use.

Question: Did you fix the keyboard delay you've got while speech catches what you're saying?

Sometimes, when the speed recognition kicks in, my computer gets busy. When my computer gets really busy, it doesn't process my keystrokes in the right order, which is very annoying because then I have to delete the previous word and retype it. I haven't sorted that out yet, but it seems like I probably have to lower the priority on different processes. On the plus side, as I mentioned, if I dictate things instead of typing them, then I don't run into that problem at all.

Also, other notes on delays: The continuous speech recognition via Google Chrome shows up fairly quickly, but it's not very precise, and it doesn't have punctuation. Even if there's a little bit of a delay, as long as I press the my-whisper-continue shortcut after each thought, then I can get that text into my Emacs buffer using the nicer transcription from my selected model. There is going to be a bit of a delay for that one because it gets processed at the end of the thought. Also, I need to start thinking in complete sentences instead of just adding one cause after the other as my brain goes on all of these tangents. I think it's pretty promising. There's the continuous speech recognition via Google Chrome if I don't mind the lower accuracy and lack of punctuation, and I can still get the pretty version on the other side.

Why talk out loud? I liked the Bookclub Tapas presentation that Maddie Sullivan did at EmacsConf 2025. Talking out loud helps me be a lot more verbose about what I'm saying, compared to typing things out or even like having to switch to my notes or interrupting my screen with an Org capture buffer. Of course I want to clean that up for putting into a blog post, but given that my life still sometimes has random interruptions from a kiddo who must have my attention at that very minute, having that kind of record that I can at least try to reread afterwards to reconstruct what I was thinking about sounds like it might be helpful.

Still, making sense out loud is hard. I'm not actually used to talking to people that much now. This is probably a good reason for me to experiment with streaming more. Then I get the practice in talking out loud, there are backup recordings, and people can ask questions when things are unclear.

Of course, sometimes the text doesn't quite make sense because of the speech recognition errors. I can usually figure it out from the context. I save the audio as well so that I can go back and listen to it again if I really need to.

Anyway, here's the code for sending the current recording to whisper in the background and starting another recording. It assumes a lot about how things are set up. For example, I'm only testing this with a local speaches server instead of whisper.cpp. You might need to look at my other speech related configuration blog posts and sections in order to make sense of it.

Code for queuing whisper.el requests to a local server
(defvar my-whisper--queue nil)
(defun my-whisper-continue (&optional arg)
  "Send what we've got so far for transcription and then continue recording.
Call with \\[universal-argument] to signal that we can stop."
  (interactive "P")
  (setq whisper--marker (point-marker) whisper--point-buffer (current-buffer))
  (when (process-live-p whisper--recording-process)
    (add-to-list
     'my-whisper--queue
     (list :file whisper--temp-file
           :buffer
           (format "*result: %s*" (file-name-base whisper--temp-file)))
     t)
    ;; Remove the sentinel; handle results ourselves
    (set-process-sentinel whisper--recording-process
                          (lambda (process event)
                            (my-whisper-process-queue)))
    (interrupt-process whisper--recording-process))
  (unless arg
    (run-hooks 'whisper-before-transcription-hook)
    (whisper--record-audio)))

(defun my-whisper-discard ()
 "Ignore the previous recording."
  (interactive)
  (when (process-live-p whisper--recording-process)
    ;; Remove the sentinel; handle results ourselves
    (set-process-sentinel whisper--recording-process
                          (lambda (process event)
                            (when (file-exists-p whisper--temp-file)
                              (delete-file whisper--temp-file))
                            (my-whisper-process-queue)))
    (interrupt-process whisper--recording-process)))

(defun my-whisper-discard-and-continue ()
 "Ignore the previous recording and continue."
  (interactive)
  (if (process-live-p whisper--recording-process)
      (progn
        ;; Remove the sentinel; handle results ourselves
        (set-process-sentinel whisper--recording-process
                              (lambda (process event)
                                (my-whisper-process-queue)
                                (my-whisper-continue)))
        (interrupt-process whisper--recording-process))
    (my-whisper-continue)))

(defun my-whisper-done ()
  (interactive)
  (when (process-live-p whisper--recording-process)
    (add-to-list
     'my-whisper--queue
     (list :file whisper--temp-file
           :buffer
           (format "*result: %s*" (file-name-base whisper--temp-file)))
     t)
    ;; Remove the sentinel; handle results ourselves
    (set-process-sentinel whisper--recording-process
                          (lambda (process event)
                            (my-whisper-process-queue)))
    (interrupt-process whisper--recording-process)))

(defun my-whisper-process-queue-result ()
  "Process the first part of the queue that already has results."
  (while (plist-get (car my-whisper--queue) :results)
    (let ((o (pop my-whisper--queue)))
      (unless my-whisper-target-markers
        (setq whisper--marker (point-marker)
              whisper--point-buffer (current-buffer)))
      (with-current-buffer (plist-get o :buffer)
        (erase-buffer)
        (insert (plist-get o :results))
        (goto-char (point-min))
        (run-hook-wrapped
         'whisper-after-transcription-hook
         (lambda (f)
           (with-current-buffer (get-buffer (plist-get o :buffer))
             (save-excursion
               (funcall f)))
           nil))))))

(defun my-whisper-process-queue ()
  (let (o)
    (while (setq o (seq-find (lambda (o) (and (plist-get o :file)
                                              (not (plist-get o :process))
                                              (not (plist-get o :results))))
                             my-whisper--queue))
      (let* ((headers (list "Content-Type: multipart/form-data"))
             (params (list (concat "file=@"
                                   (plist-get o :file))
                           "temperature=0.0"
                           "temperature_inc=0.2"
                           "response_format=json"
                           (concat "model=" whisper-model)
                           (concat "language=" whisper-language)))
             (url (format my-whisper-url-format whisper-server-host whisper-server-port))
             (command `("curl" "-s"
                        ,url
                        ,@(mapcan (lambda (h) (list "-H" h)) headers)
                        ,@(mapcan (lambda (p) (list "-F" p)) params))))
        (with-current-buffer (get-buffer-create (plist-get o :buffer))
          (erase-buffer))
        (plist-put
         o :process
         (make-process
          :name "whisper-curl"
          :command command
          :buffer (plist-get o :buffer)
          :coding 'utf-8
          :sentinel
          (lambda (process event)
            (with-current-buffer (process-buffer process)
              (let ((current my-whisper--queue-item))
                (when (and (get-buffer (plist-get current :buffer))
                           (string-equal "finished\n" event))
                  (with-current-buffer (plist-get current :buffer)
                    (goto-char (point-min))
                    (plist-put current :results
                               (or
                                (condition-case nil
                                    (gethash "text" (json-parse-buffer))
                                  (error ""))
                                "(error)"))))))
            (my-whisper-process-queue-result))))
        (plist-put o :command (string-join command " "))
        (with-current-buffer (process-buffer (plist-get o :process))
          (setq-local my-whisper--queue-item o))))))
(defvar-local my-whisper--queue-item nil)

(defun my-whisper-reprocess-queue ()
  (interactive)
  (setq whisper--marker (point-marker) whisper--point-buffer (current-buffer))
  (mapc (lambda (o)
          (when (process-live-p (plist-get o :process))
            (kill-process (plist-get o :process)))
          (when (get-buffer (plist-get o :buffer))
            (kill-buffer (plist-get o :buffer)))
          (plist-put o :process nil)
          (plist-put o :results nil))
        my-whisper--queue)
  (my-whisper-process-queue))

(defun my-whisper-clear-queue ()
  (interactive)
  (mapc (lambda (o)
          (when (process-live-p (plist-get o :process))
            (kill-process (plist-get o :process)))
          (when (get-buffer (plist-get o :buffer))
            (kill-buffer (plist-get o :buffer)))
          (plist-put o :process nil)
          (plist-put o :results nil))
        my-whisper--queue)
  (setq my-whisper--queue nil))

(keymap-global-set "<kp-9>" #'my-whisper-continue)
(keymap-global-set "<kp-8>" #'my-whisper-discard-and-continue)
(keymap-global-set "C-<kp-9>" #'my-whisper-done)
This is part of my Emacs configuration.
View org source for this post

Emacs and whisper.el: Trying out different speech-to-text backends and models

| audio, emacs

I was curious about parakeet because I heard that it was faster than Whisper on the HuggingFace leaderboard. When I installed it and got it running on my laptop (CPU only, no GPU), it seemed like my results were a little faster than whisper.cpp with the large model, but much slower than whisper.cpp with the base model. The base model is decent for quick dictation, so I got curious about other backends and other models.

In order to try natrys/whisper.el with other backends, I needed to work around how whisper.el validates the model names and sends requests to the servers. Here's the quick and dirty code for doing so, in case you want to try it out for yourself.

(defvar my-whisper-url-format "http://%s:%d/transcribe")
(defun my-whisper--transcribe-via-local-server ()
  "Transcribe audio using the local whisper server."
  (message "[-] Transcribing via local server")
  (whisper--setup-mode-line :show 'transcribing)
  (whisper--ensure-server)
  (setq whisper--transcribing-process
        (whisper--process-curl-request
         (format my-whisper-url-format whisper-server-host whisper-server-port)
         (list "Content-Type: multipart/form-data")
         (list (concat "file=@" whisper--temp-file)
               "temperature=0.0"
               "temperature_inc=0.2"
               "response_format=json"
               (concat "model=" whisper-model)
               (concat "language=" whisper-language)))))
(defun my-whisper--check-model-consistency () t)

(with-eval-after-load 'whisper
  (advice-add 'whisper--transcribe-via-local-server :override #'my-whisper--transcribe-via-local-server)
  (advice-add 'whisper--check-model-consistency :override #'my-whisper--check-model-consistency))

Then I have this function for trying things out.

(defun my-test-whisper-api (url &optional args)
  (with-temp-buffer
    (apply #'call-process "curl" nil t nil "-s"
           url
         (append (mapcan
                  (lambda (h) (list "-H" h))
                  (list "Content-Type: multipart/form-data"))
                 (mapcan
                  (lambda (h) (list "-F" h))
                  (list (concat "file=@" whisper--temp-file)
                        "temperature=0.0"
                        "temperature_inc=0.2"
                        "response_format=verbose_json"
                        (concat "language=" whisper-language)))
                 args))
    (message "%s %s" (buffer-string) url)))

Here's the audio file. It is around 10 seconds long. I run the benchmark 3 times and report the average time.

Download

Code for running the benchmarks
(let ((times '3))
(mapcar
 (lambda (group)
   (let ((whisper--temp-file "/home/sacha/recordings/whisper/2026-01-19-14-17-53.wav"))
     ;; warm up the model
     (eval (cadr group))
     (list
      (format "%.3f"
              (/ (car
                  (benchmark-call (lambda () (eval (cadr group))) times))
                 times))
      (car group))))
 '(
   ("parakeet"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 5092)))
   ("whisper.cpp base-q4_0"
    (my-test-whisper-api
     (format "http://%s:%d/inference" whisper-server-host 8642)))
   ("speaches whisper-base"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-base")))
   ("speaches whisper-base.en"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-base.en")))
   ("speaches whisper-small"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-small")))
   ("speaches whisper-small.en"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=Systran/faster-whisper-small.en")))
   ("speaches lorneluo/whisper-small-ct2-int8"
    (my-test-whisper-api
     (format "http://%s:%d/v1/audio/transcriptions" whisper-server-host 8001)
     (list "-F" "model=lorneluo/whisper-small-ct2-int8")))
   ;; needed export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1
   ("whisperx-server Systran/faster-whisper-small"
    (my-test-whisper-api
     (format "http://%s:%d/transcribe" whisper-server-host 8002)))))
)

I tried it with:

Looks like speaches + faster-whisper-base is the winner for now. I like how speaches lets me switch models on the fly, so maybe I can use base.en generally and switch to base when I want to try dictating in French. Here's how I've set it up to use the server I just set up.

(setq whisper-server-port 8001 whisper-model "Systran/faster-whisper-base.en"
      my-whisper-url-format "http://%s:%d/v1/audio/transcriptions")

At some point, I'll override whisper--ensure-server so that starting it up is smoother.

Benchmark notes: I have a Lenovo P52 laptop (released 2018) with an Intel Core i7-8850H (6 cores, 12 threads; 2.6 GHz base / 4.3 GHz turbo) with 64GB RAM and an SSD. I haven't figured out how to get the GPU working under Ubuntu yet.

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

2026-01-19 Emacs news

| emacs, emacs-news

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

La semaine du 12 janvier au 18 janvier

| french

Lundi, le douze janvier

Une réflexion sur les points de choix que ma thérapeute a discutés avec moi :

Je pense que l'idée du point de choix concerne le moment où on a un objectif, mais on se rend souvent compte qu'on agit à contre-courant à cause des réactions. Je rencontre parfois ce problème dans ma vie, quand je veux soutenir ma fille à l'autodétermination mais je m'inquiète aussi pour ses devoirs, ses notes, ses interactions avec son enseignant et ainsi de suite.

Ma thérapeute m'a demandé de réfléchir aux situations qui sont difficiles et ce qui m'inquiète.

Je pense que la situation est difficile quand :

  • … ma fille dit qu'elle déteste l'école et elle me demande pourquoi elle doit y aller, par exemple à l'heure du coucher dimanche
  • … ma fille ne travaille pas sur ses devoirs et elle accumule alors un gros tas de tâches. Par exemple, c'est la semaine avant une journée pédagogique que l'enseignant va probablement utiliser pour écrire les bulletins.
  • … ma fille veut passer du temps avec moi mais je suis fatiguée ou surstimulée, ou je veux me concentrer sur d'autres choses, par exemple après quelques heures ensemble lundi. ( En vrai, c'est un problème de riche : ma fille aime bien passer du temps avec moi. )
  • … ma fille est grincheuse parce qu'elle veut jouer avec moi mais je suis occupée, par exemple quand je dois travailler sur l'infolettre de Bike Brigade samedi.
  • … ma fille est fâchée parce que quelqu'un fait une erreur, et elle a du mal à se débrouiller.

Je me demande de temps en temps ce qu'un parent responsable fait. Qu'est-ce qui m'inquiète? Par exemple, sur ses devoirs :

  • Si elle ne fait pas ses devoirs, son enseignant doit lui donner des notes basses. (Mais je ne sais pas, ses camarades ne paraissent pas faire leurs devoirs non plus selon les communiqués fréquents.)
  • … Puis peut-être qu'elle décide que l'école ne vaut pas le coup.
  • … Puis peut-être qu'elle a du mal à s'adapter au système d'éducation ou d'emploi.
    • … Puis peut-être qu'elle se sent en échec, perd la motivation, et elle se laisse porter. (Mais ce n'est pas sous mon contrôle.)
    • … Puis peut-être qu'elle doit trouver sa propre voie dans l'entrepreneuriat. (Mais ce n'est pas grave…)

Ma thérapeute va dire que oui, mais qu'est-ce que ça signifie pour vous ? Hmm… Je veux l'aider du mieux que je peux, sans lui transmettre mes difficultés et défauts, ce qui est amusant parce que l'une de mes difficultés est l'anxiété. Elle n'est pas anxieuse pour ses études… Succès ?

Peut-être que je ne pose pas la bonne question quand je me demande ce qu'un parent responsable fait. Peut-être que je me focalise trop sur les objectifs à court terme. La conformité est un objectif clair et facile à mesurer, mais suis-je sûr que c'est bon pour elle à long terme ? Je veux l'aider à apprendre à trouver ses propres façons de faire les choses qui sont nécessaires, mais je veux aussi l'aider à apprendre à juger ce qui est nécessaire.

Élever des enfants, c'est vraiment apprendre à lâcher prise en permanence.

-— Lors de ma séance avec ma tutrice, nous avons juste pu réviser mes entrées de la dernière semaine à l'exception de la réflexion ci-dessus. Pour une raison quelconque, mes titres n'ont pas été copiés. Je vais corriger des bogues dans mon logiciel pour les exporter plus tard.

Après l'école, ma fille a dit que ses camarades étaient trop bruyants. Heureusement, dans l'école virtuelle, elle peut réduire le volume jusqu'à ce que ce soit tolérable. Parfois j'aimerais que la vraie vie soit comme ça.

J'ai emmené ma fille à son cours de gymnastique. Pendant qu'elle s'entraînait à faire la roue, j'ai réfléchi encore sur mes points de choix.

Ma fille a encore fait la factrice. Elle a distribué des ingrédients pour le souper et d'autres choses partout dans la maison.

À l'heure du coucher, elle est devenue très fâchée parce que nous avions oublié de nettoyer ses oreilles pendant la pause déjeuner. Elle s'est assise contre la porte et m'a crié : « Dégage ! » Ben, je ne peux pas résoudre tous les problèmes. La prochaine fois, je mettrai un minuteur pour nous en souvenir. J'ai offert un câlin au cas où elle en voudrait un. J'ai passé du temps avec mon mari à essayer d'échanger des Pokémon, mais ça n'a pas marché.

Mardi, le treize janvier

Malgré mon petit glaçon qui s'est blotti contre moi toute la nuit comme je l'avais prédit, nous nous sommes levées à une heure raisonnable et nous avons fait notre routine matinale.

Pendant que ma fille participait à l'école virtuelle, j'ai continué à essayer la reconnaissance vocale en continu. J'ai testé SimulStreaming avec un petit modèle et un moyen modèle, mais les résultats étaient pires que ceux de Web Speech API sous Google Chrome. Je suis repassé sur Chrome et j'ai redirigé les résultats vers le salon IRC et vers Etherpad. J'ai testé le routage audio pour la transcription de flux multiples en même temps. C'est prometteur, mais je ne sais pas si mon ordinateur pourra gérer quatre ou cinq réunions en ligne pendant EmacsConf. Je peux le tester en l'essayant avec les vidéos de la dernière conférence.

Je veux présenter le flux de travail pour insérer les résultats de reconnaissance vocale à la position actuelle ou à la tâche active dans Org Mode à la réunion OrgMeetup en ligne demain après-midi. Si nous avons du temps, je peux aussi présenter le flux de travail de la reconnaissance vocale en continu, ce qui est étonnamment utile pour me souvenir de ce que je viens de penser.

Après le déjeuner, j'ai utilisé la reconnaissance vocale (en mode intermittent ou continu) pour préparer ma séance virtuelle avec ma thérapeute. J'ai réfléchi sur des points de choix dans des situations difficiles, mes sentiments, mes pensées, et des actions que je veux faire la prochaine fois. Nous en avons discuté pendant ma séance. Ma thérapeute m'a aussi parlé des distorsions cognitives et du perfectionnisme.

Pour le souper, nous avons préparé des nouilles udon. Après ça, ma fille a regardé Cleopatra in Space, qui est différente des livres mais est toujours amusante.

Mercredi, le quatorze janvier

J'ai présenté mon travail sur l'intégration de la reconnaissance vocale et Org Mode à la réunion OrgMeetup en ligne. J'ai aussi présenté mon expérience de la reconnaissance vocale en continu sous Google Chrome. Un participant m'a demandé si Google Chrome ou Chromium peut faire la reconnaissance vocale en continu en local au lieu de sur le cloud. Il paraît que c'est possible après avoir téléchargé les modèles de langue. L'après-midi, j'ai configuré un raccourci clavier global pour dicter dans d'autres applications via le flux de travail de la reconnaissance vocale sous Emacs.

J'ai aussi travaillé sur l'outil de visualisation et la gestion du routage audio. J'ai créé une fonction pour changer les entrées d'un nœud. J'ai publié un article avec quelques captures d'écran sur mon blog.

Après l'école, ma fille et moi avons fait des muffins à la courgette et au chocolat. Elle a assemblé une collation et me l'a vendue avec quatre muffins. Ça coûte dix tope-làs au total.

Nous avons rejoint l'amie de ma fille et sa troupe de scouts à dix-neuf heures à la patinoire. Il y avait beaucoup de neige, donc les enfants ont aimé déneiger avec des guides de patinage faisant office de Zamboni. Ma fille a eu un petit conflit avec un autre enfant à propos du jeu du marchand et ils ont lancé de la neige, donc on surveillait ça de près.

Ma fille n'avait pas très faim aujourd'hui. Eh, ce n'était pas grave, elle va probablement manger davantage demain.

Jeudi, le quinze janvier

Grâce à l'énorme quantité de neige, l'école a été annulée. Elle était ravie ! Nous avons déneigé les chemins, le trottoir et la terrasse en bois pendant plus d'une heure. Nous avons empilé la neige sur la luge pour traîner jusqu'à la cour arrière, où nous l'avons ajoutée à un énorme tas. Il continuait de neiger. Le voisin dit qu'il va utiliser la mini souffleuse à neige plus tard, et la déneigeuse de la ville passera probablement aussi.

J'ai modifié mon outil d'enregistrement des audios à partir des sous-titres pour générer de la synthèse vocale du texte puis commencer à enregistrer, et assembler les versions finales automatiquement en un seul fichier audio sans faux départs. Ça me plaît. Pour ma prochaine étape, je veux combiner la reconnaissance vocale en continu avec ça alors qu'Emacs dira la phrase, enregistrera mon essai et avancera automatiquement si je prononce de manière assez compréhensible ou répétera si je le prononce trop mal. Idéalement, il surlignera les mots que je prononce mal, mais je ne sais pas encore comment m'y prendre. Je l'ai utilisé pour enregistrer les entrées de la semaine dernière en français.

Un jour, je pourrai les publier sur mon blog avec les textes. Je suis un peu intimidée, mais essayer davantage, c'est apprendre davantage.

Nous avons joué à Pokémon. Ma fille et moi avons découvert une bonne façon de jouer en mode cocooning et très complice : elle s'est blottie contre moi sur le canapé et elle a manipulé la moitié droite de la manette tandis que j'ai manipulé la moitié gauche. Comme ça, nous avons transformé le jeu solo en jeu coopératif, que ma fille aime mieux, et nous étions plus au chaud.

J'ai regardé deux épisodes de Bluey qui sont doublés en français. C'était toujours trop rapide pour comprendre la majorité, mais quelquefois j'ai compris des mots. Je n'ai pas pu configurer correctement ma télévision pour l'affichage du sous-titrage officiel en français, mais j'ai trouvé les sous-titres pour l'épisode Le Vélo en ligne grâce à un fan, ce qui m'a aidée à le comprendre.

Ma fille a encore joué à la factrice. Elle m'a distribué de l'eau chaude, des chaussettes pelucheuses pour moi et des gouttes pour ses yeux. Elle a aussi apporté tous les livres qu'elle avait lus hier soir pour les remplacer par de nouveaux livres.

Vendredi, le seize janvier

Ce matin, j'ai essayé d'aider ma fille avec son devoir d'art. Elle doit créer une histoire sur la fête. Elle ne voulait pas dessiner à la main, alors j'ai suggéré d'utiliser Minecraft pour narrer l'histoire pour que nous puissions prendre des captures d'écran. Malheureusement, Minecraft Bedrock ne marchait pas. Elle est devenue frustrée et ne voulait pas continuer.

J'ai utilisé la reconnaissance vocale en continu pour contrôler l'enregistrement de l'audio. Si Emacs entend quelque chose qui ressemble au sous-titre actuel, il mettra à jour ses horodatages, utilisera la synthèse vocale pour dire le prochain sous-titre et attendra que je le dise. S'il entend quelque chose qui ressemble plus au sous-titre précédent, il retournera le sous-titre précédent et mettra à jour ses horodatages. S'il entend quelque chose qui ressemble plus au prochain sous-titre, il avancera le prochain sous-titre, mettra à jour ses horodatages, avancera encore à celui d'après et le dira. Je pense que je dois modifier le seuil de correspondance approximative parce que la reconnaissance vocale en continu n'est pas très précise. J'ai essayé les résultats intermédiaires de reconnaissance vocale pour l'horodatage initial, mais si l'énoncé était très court, l'horodatage initial est peut-être manquant ou inexact. Il faudra peut-être que j'analyse les silences dans le fichier audio pour déterminer un horodatage initial raisonnable.

Après que ma fille s'est remise de sa frustration, nous avons fait de la luge. C'était amusant, mais il y avait beaucoup de monde, donc nous ne sommes pas restées longtemps. Quand nous sommes rentrées à la maison, elle voulait toujours jouer dehors, donc j'ai déneigé et elle a utilisé son grand bâton pour piler la glace sur le chemin.

Pendant que mon mari et ma fille faisaient les courses pour des bombes de chocolat chaud et d'autres choses, j'ai travaillé comme consultante. En résolvant une tâche que j'avais mise de côté depuis deux mois parce qu'elle n'était pas urgente, j'ai découvert une requête urgente que je dois traiter cette semaine pour un événement qui aura lieu la semaine prochaine. Je ne consulte pas souvent mes courriels professionnels, donc j'ai failli la rater. Ils ont besoin du plan de salle pour des événements. Heureusement, j'ai pu l'accomplir rapidement.

Après avoir fini, j'ai joué avec ma fille. Mon mari a trouvé comment échanger des Pokémon en changeant de cœur d'émulateur. Nous lui avons donné un Abra en échange d'un Rondoudou, et nous avons pu attraper un autre Abra un peu plus tard. Malheureusement, l'Abra que nous lui avons donné n'obéit pas à mon mari. Il se trouve qu'il a besoin d'un Badge d'Arène de plus. Bon, nous sommes allées à Carmin sur Mer. Nous avons obtenu un vélo, exploré le bateau L'Océane, et réussi à battre l'arène. Maintenant nous pouvons utiliser la Coupe pour couper les arbustes et accéder à de nouveaux endroits. Elle n'aime pas s'entraîner, mais ça ne me dérange pas, donc je suis heureuse de faire progresser ses Pokémon.

Samedi, le dix-sept janvier

Pour le petit-déjeuner, j'ai préparé des crêpes. Ma fille a mangé deux crêpes avec du yaourt à la mangue et une crêpe avec de la tartinade aux noisettes.

J'ai utilisé mes fonctions pour transformer, vérifier, et planifier la campagne Mailchimp, et J'ai écrit un article sur ma gestion de la campagne Mailchimp sous Emacs.

J'ai essayé le jeu Pokémon Rouge sur mon mobile. C'était assez facile à comprendre. J'ai été très étonnée quand le jeu m'a demandé comment je m'appelle, parce que Sacha est une des options. Je me suis demandé si le jeu avait analysé mon mobile ou si le jeu est modifié pour utiliser Google Play. Il se trouve que dans la série télévisée Pokémon en français, le protagoniste s'appelle Sacha, qui correspond à Ash dans la série en anglais. Quelle surprise !

Ma fille n'a pas voulu aller à son club nature au parc. Nous avons toujours voulu sortir. Elle était curieuse du jeu Pokémon Go, donc nous l'avons installé et j'ai connecté son mobile à mon point Wi-Fi sur mon mobile. Nous avons attrapé beaucoup de Pokémon en allant faire les courses. Mon mari a aussi apporté une pelle, et nous avons déneigé des trottoirs et des coins des rues dans notre voisinage en alternance pendant que l'autre et ma fille jouaient à Pokémon Go. Ça faisait du bien de faire quelque chose pour aider notre voisinage. (J'ai lu que d'autres gens ont déneigé des pistes cyclables que les déneigeuses ont mal déneigées. Bravo !) C'était des bonnes raisons pour sortir, même s'il y avait de la neige.

Ma fille avait beaucoup d'enthousiasme et elle s'est amusée avec la réalité augmentée. Elle a proposé de faire une promenade après le souper pour attraper encore des Pokémon.

Pour le souper, mon mari et notre fille ont préparé du riz gluant au poulet et aux champignons. Pendant qu'ils cuisinaient, j'ai farmé les Pokémon. Malheureusement, j'ai fait oublier Berceuse à Rondoudou parce que j'avais appuyé sur les boutons trop rapidement sans faire attention quand le jeu m'a demandé si je voulais qu'il apprenne Repos. J'ai restauré la sauvegarde et refait une partie du jeu.

J'ai fait la vaisselle rapidement pour faire une promenade après le souper. Malheureusement, nous avons laissé les mitaines de ma fille au supermarché. Nous allons les chercher demain, ou si nous ne pouvons pas les trouver, nous allons en racheter d'autres aux Stockyards. Malgré le froid, nous avons réussi à explorer un peu plus.

Dimanche, le dix-huit janvier

Tôt le matin, nous sommes allés au supermarché pour chercher les mitaines de notre fille que nous avons accidentellement laissées là hier. Malheureusement, nous n'avons pas pu les trouver. Ce n'était pas grave. Nous avons fait du vélo au Dufferin Mall pour les racheter chez Carters. Bien que les pistes cyclables étaient trop fondues et avaient du verglas, faire du vélo était un bon choix parce que le métro était fermé, donc les navettes étaient très bondées. Heureusement, le magasin avait des mitaines de même style et de même taille, et elles étaient même en solde. Elle a aussi voulu acheter un démêlant pour enfants chez Sephora, et elle a reçu un cadeau gratuit d'anniversaire. Elle a choisi l'option la plus simple.

Nous sommes rentrés et j'ai préparé nos déjeuners au cas où nous aurions faim après le cours de patinage ou l'événement de Pokémon Go auquel nous voulions participer. J'ai emmené ma fille à la patinoire juste à l'heure pour son cours de patinage. On a découvert que son autre amie s'était aussi inscrite au même cours. Elle avait dû attendre parce que sa mère avait pris le mauvais casque, donc je l'ai accompagnée en attendant pendant que sa mère s'était dépêchée jusqu'à ce qu'elle puisse participer au cours avec son propre casque.

Après le cours et un peu de patinage, nous l'avons invitée à la maison pour déjeuner, jouer, et boire du chocolat chaud au lieu de participer à l'événement de Pokémon Go. Elles ne jouaient pas ensemble depuis longtemps. Elles ont joué au petit café et aux marchands, et j'étais la cliente. Pendant qu'elles jouaient aux LEGO, j'ai continué mon propre jeu de Pokémon en français. Après que le père de son amie est venu la chercher, ma fille a voulu sortir pour attraper des Pokémons et essayer de suivre toute une route, donc nous avons fait une promenade. Puis, nous avons combattu. Elle avait attrapé une Lippoutou qui est plus forte que mes Pokémon. Elle a toujours gagné, ce qui lui faisait plaisir.

Après qu'elle a pris une douche avec son nouveau démêlant, je lui ai brossé et tressé les cheveux pendant qu'elle jouait Pokémon Jaune à la télévision. Je pense que nos Pokémon étaient un peu de bas niveau, mais elle a réussi à traverser le tunnel rocheux et elle est arrivée au Centre Pokémon de l'autre côté. Elle a gagné en confiance quand elle a combattu des dresseurs.

À cause de la journée chargée et de nos occupations, nous avons oublié de faire la lessive. Bon, je vais la faire demain.

Notes

  • Prononciation :
    • … et elle a du mal à se débrouiller. (day broo yay)
    • Peut-être que je ne pose pas la bonne question (kes tion) quand je me demande ce qu'un parent responsable fait.
    • … ce qui est étonnamment utile pour me souvenir de ce que je viens de penser. (pehn say)
    • J'ai aussi travaillé sur l'outil (loo teel) de visualisation et la gestion du routage audio.
    • J'ai publié un article (ar tee kleuh) avec quelques captures d'écran sur mon blog.
    • Elle m'a distribué de l'eau chaude, des chaussettes pelucheuses pour moi et des gouttes pour ses yeux. (says yeuh)
    • … si l'énoncé était très court, l'horodatage initial est peut-être manquant (man kahn) ou inexact (ein ex act)
    • Il faudra peut-être que j'analyse les silences (see lehns) dans le fichier audio pour déterminer un horodatage initial raisonnable.
    • Je ne consulte pas souvent mes courriels professionnels, donc j'ai failli (faie yee) la rater.
    • Ils ont besoin du plan de salle (sal) pour des événements.
    • … nous avons déneigé des trottoirs (trawh twoirs) et des coins des rues dans notre voisinage
  • Grammaire et expressions :
    • … le rendez-vous de Pokémon Go auquel nous voulions participer - because participer takes à, also voulions is in subjonctif
    • vers for directing streams towards
    • facile à infinitif - easy to …
    • sans infinitif - without
    • en permanence - all the time
View org source for this post

Emacs: Updating a Mailchimp campaign using a template, sending test e-mails, and scheduling it

| emacs

I'm helping other volunteers get on board with doing the Bike Brigade newsletter. Since not everyone has access to (or the patience for) MailChimp, we've been using Google Docs to draft the newsletter and share it with other people behind the scenes. I've previously written about getting a Google Docs draft ready for Mailchimp via Emacs and Org Mode, which built on my code for transforming HTML clipboard contents to smooth out Mailchimp annoyances: dates, images, comments, colours. Now I've figured out how to update, test, and schedule the MailChimp campaign directly from Emacs so that I don't even have to go into the MailChimp web interface at all. I added those functions to sachac/mailchimp-el.

I used to manually download a ZIP of the Google Docs newsletter draft. I didn't feel like figuring out authentication and Google APIs from Emacs, so I did that in a NodeJS script instead. convert-newsletter.js can either create or download the latest newsletter doc from our Google Shared Drive. (google-api might be helpful if I want to do this in Emacs, not sure.) If I call convert-newsletter.js with the download argument, it unpacks the zip into ~/proj/bike-brigade/temp_newsletter, where my Emacs Lisp function for processing the latest newsletter draft with images can turn it into the HTML to insert into the HTML template I've previously created. I've been thinking about whether I want to move my HTML transformation code to NodeJS as well so that I could run the whole thing from the command-line and possibly have other people run this in the future, or if I should just leave it in Emacs for my convenience.

Updating the campaign through the Mailchimp API means that I don't have to log in, replicate the campaign, click on the code block, and paste in the code. Very nice, no clicks needed. I also use TRAMP to write the HTML to a file on my server (my-bike-brigade-output-file is of the form /ssh:hostname:/path/to/file) so that other volunteers can get a web preview without waiting for the test email.

(defun my-brigade-next-campaign (&optional date)
  (setq date (or date (org-read-date nil nil "+Sun")))
  (seq-find
   (lambda (o)
     (string-match (concat "^" date)
                   (alist-get 'title (alist-get 'settings o))))
   (alist-get 'campaigns (mailchimp-campaigns 5))))

(defvar my-bike-brigade-output-file nil)

(defun my-brigade-download-newsletter-from-google-docs ()
  "Download the newsletter from Google Docs and puts it in ~/proj/bike-brigade/temp_newsletter/."
  (interactive)
  (let ((default-directory "~/proj/bike-brigade"))
    (with-current-buffer (get-buffer-create "*Newsletter*")
      (erase-buffer)
      (display-buffer (current-buffer))
      (call-process "node" nil t t "convert-newsletter.js" "download"))))

(defun my-brigade-create-or-update-campaign ()
  (interactive)
  (let* ((date (org-read-date nil nil "+Sun"))
         (template-name "Bike Brigade weekly update")
         (list-name "Bike Brigade")
         (template-id
          (alist-get
           'id
           (seq-find
            (lambda (o)
              (string= template-name (alist-get 'name o)))
            (alist-get 'templates (mailchimp--request-json "templates")))))
         (list-id (seq-find
                   (lambda (o)
                     (string= list-name
                              (alist-get 'name o)))
                   (alist-get 'lists (mailchimp--request-json "lists"))))
         (campaign (my-brigade-next-campaign date))
         (body `((type . "regular")
                 (recipients (list_id . ,(alist-get 'id list-id)))
                 (settings
                  (title . ,date)
                  (subject_line . "Bike Brigade: Weekly update")
                  (from_name . "Bike Brigade")
                  (reply_to . "info@bikebrigade.ca")
                  (tracking
                   (opens . t)
                   (html_clicks . t))))))
    (unless campaign
      (setq campaign (mailchimp--request-json
                      "/campaigns"
                      :method "POST"
                      :body
                      body)))
    ;; Download the HTML
    (my-brigade-download-newsletter-from-google-docs)
    ;; Upload to Mailchimp
    (mailchimp-campaign-update-from-template
     (alist-get 'id campaign)
     template-id
     (list
      (cons "main_content_area"
            (my-brigade-process-latest-newsletter-draft-with-images
             date))))
    (when my-bike-brigade-output-file
      (with-temp-file my-bike-brigade-output-file
        (insert (alist-get 'html (mailchimp--request-json (format "/campaigns/%s/content" (alist-get 'id campaign)))))))
    (browse-url (concat "https://sachachua.com/bike-brigade/" (file-name-nondirectory my-bike-brigade-output-file)))
    (message "%s" "Done!")))

Now to send the test e-mails…

(defvar my-brigade-test-emails nil "Set to a list of e-mail addresses.")
(defun my-brigade-send-test-to-me ()
  (interactive)
  (mailchimp-campaign-send-test-email (my-brigade-next-campaign) user-mail-address))

(defun my-brigade-send-test ()
  (interactive)
  (if my-brigade-test-emails
      (mailchimp-campaign-send-test-email (my-brigade-next-campaign) my-brigade-test-emails)
    (error "Set `my-brigade-test-emails'.")))

And schedule it:

(defun my-brigade-schedule ()
  (interactive)
  (let* ((campaign (my-brigade-next-campaign))
         (sched (format-time-string "%FT%T%z" (org-read-date t t "+Sun 11:00") t)))
    (mailchimp-campaign-schedule campaign sched)
    (message "Scheduled %s" (alist-get 'title (alist-get 'settings campaign)))))

Progress, bit by bit! Here's a screenshot showing the Google Docs draft on one side and my web preview in the other:

2026-01-17_13-00-27.png
Figure 1: Google Docs and Mailchimp campaign preview

It'll be even cooler if I can get some of this working via systemd persistent tasks so that they happen automatically, or have some kind of way for the other newsletter volunteers to trigger a rebuild. Anyway, here's https://github.com/sachac/mailchimp-el in case the code is useful for anyone else.

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