Semaine du 28 septembre au 4 octobre

| french

lundi 28

Avant ma séance avec un nouveau tuteur, j'ai rassemblé des entrées de mon journal et les corrigés. Il y avait même une journée entière où je n'ai rien noté. Heureusement, j'ai pu reconstruire ma journée avec l'aide d'autres données. journal:2026-09-28-01

J'ai fait ma première diffusion en direct avec mes nouvelles fonctions pour contrôler OBS à la voix, ce qui inclut de commencer et d'arrêter la diffusion, d'extraire la dernière minute ou d'autres segments et de saisir des tâches. C'était très utile parce que pour la première fois j'ai saisi toutes les petites tâches qui ont détourné mon attention. J'ai ajouté des commandes vocales pour sauter à une ligne ou un mot et pour copier des symboles. journal:2026-09-28-03

Ma fille a géré l'école indépendamment. Je suis fière d'elle. journal:2026-09-28-04

J'ai appelé l'hôpital pour enquêter sur la médication de ma fille. La coordinatrice m'a dit qu'elle n'avait pas reçu le fax, donc j'ai envoyé un autre courriel à la pharmacie. journal:2026-09-28-05

La nourrice de ma fille n'a pas répondu à mes messages depuis un peu plus d'une semaine. Peut-être qu'elles sont en vacances, ou peut-être que les filles veulent jouer ailleurs en ce moment. Ma fille a du mal à faire et à garder des amitiés. Bon, j'attends ses amies, mais nous sommes tous introvertis dans la famille et nous avons d'autres divertissements, donc je ne m'inquiète pas trop. journal:2026-09-28-06

mardi 29

Ma fille a voulu couper les fins de ses cheveux pour éliminer des pointes fourchues, donc je l'ai aidée. journal:2026-09-29-02

Ma fille n'a pas voulu aller à l'école en ligne toute la journée à cause de sa frustration envers ses camarades qui n'ont pas soumis leurs assessments malgré les exhortations de l'enseignant. Nous avons donc fait de l'escalade. journal:2026-09-29-03

J'ai eu ma deuxième séance avec le nouveau tuteur que j'essayais pendant les vacances de mon tuteur habituel. Cette fois, j'ai commencé avec une description des outils que j'avais implémenté après notre conversation précédente, ce qui m'a permis de mener un peu la conversation au lieu de tomber dans une position plus passive pendant que le tuteur déversait un flot d'informations. Je pense qu'il a tendance à enseigner à partir de ses propres intérêts et points forts, ce qui évidemment convient à beaucoup de gens, mais ne me convient pas entièrement. Je me demande si j'ai envie de continuer avec lui en plus de mes séances avec mon tuteur habituel, mais je suis aussi curieuse au sujet d'autres tuteurs. journal:2026-09-29-04

mercredi 30

J'ai ressorti mes notes pour gérer les vidéos d'EmacsConf. Au début, j'ai découvert que j'ai oublié de configurer une variable. Puis j'ai été étonnée quand le logiciel a fini immédiatement, mais après une investigation, je me suis rappelée que le logiciel s'exécute en coulisse. Une fois que la vidéo a été transformée et transcrite, j'ai corrigé la transcription. J'ai réenvoyé trois messages à un intervenant qui avait pensé que nous l'avions rejeté. journal:2026-09-30-01

Ma fille et moi avons joué à Minecraft sur le porche. Elle a créé un nouveau monde sans triche pour débloquer des trophées. journal:2026-09-30-03

J'ai exprimé pourquoi je demande une investigation à double exceptionnalité à l'école en ligne pour ma fille. Je pense que comme moi, elle montre des signes d'autisme. Ce n'est pas si grave, mais si elle grandit avec une meilleure compréhension d'elle-même, ce sera mieux ainsi. journal:2026-09-30-04

J'ai pris la résolution d'éliminer ou au moins réduire les faux départs dans ma parole en anglais et en français. Bien évidemment, le taux d'information est le même si je bégaye ou non, donc il vaut mieux que je fasse une pause parce que ça réduit la charge de compréhension de mon interlocuteur. Si je peux m'habituer à parler sans faux départs même si je parle très lentement, cela rend l'écoute plus facile (et plus supportable) pour mieux apprendre. journal:2026-09-30-07

Mon mari, ma fille et moi sommes allés à la réunion sociale de Bike Brigade. C'était vivant, en fait un peu trop bruyant pour nous tous. C'était juste à cause des nombreuses conversations simultanées dans un petit café sans musique. Heureusement, quelques personnes portaient des masques comme nous, très bien. Ma fille a remarqué que le Braille sur la pancarte était inefficace parce qu'il faisait face au trottoir derrière le verre. L'autre côté avait le même message mais sans braille. Nous avons parlé avec le vendeur et une fois qu'il a été d'accord, ma fille a renversé la pancarte pour qu'elle soit tournée vers l'intérieur. Après un peu de bavardage, nous sommes rentrés. Ce n'était pas notre genre d'événement, même si mon mari fait du bénévolat depuis plus d'un an. journal:2026-09-30-06

Mon mari a acheté de la cire pour graisser la chaîne de son vélo. Ma fille et moi l'avons regardé pendant qu'elle fondait. Ma fille a voulu reporter l'heure du coucher parce qu'elle voulait aider mon mari avec l'entretien de son vélo. Elle a fait l'école buissonnière deux matins de suite à cause de fatigue, mais je me rappelle que la pression ne lui sert à rien. Elle apprend les compétences pratiques, elle prend ses responsabilités (ou au moins accepte les résultats de ses décisions), c'est son expérience. journal:2026-09-30-05

jeudi 1

La coordinatrice de nos avocats m'a demandé des détails d'un paiement de juillet qu'elle n'a pas pu trouver. Heureusement, j'ai pu lui renvoyer la confirmation de la banque et elle l'a retrouvé dans ses archives, donc c'était réglé. journal:2026-10-01-02

Mon mari, ma fille et moi avons joué à Minecraft. Il nous a rejoint dans notre village. J'ai aménagé une salle d'échange, ce qui a également sécurisé des villageois contre les attaques des zombies. Je suis un peu sortie pour chercher des vaches, mais je n'ai trouvé rien. journal:2026-10-01-03

J'ai travaillé comme consultante. C'était une journée productive. J'ai fait part de mon expérience et j'ai implémenté les changements demandés. journal:2026-10-01-04

vendredi 2

J'étais fière de voir que ma fille a révisé ses devoirs elle-même. Je l'ai laissée gérer presque tout cette semaine, lui ayant même laissé faire une grasse matinée. Bien sûr, je l'ai motivée un peu en lui demandant de finir ses devoirs d'écriture avant que nous fassions de l'escalade alors qu'elle voulait sécher les cours toute la journée. journal:2026-10-02-02

J'ai envoyé le brouillon du programme d'EmacsConf aux intervenants. J'ai amélioré mes fonctions pour noter les détails que j'ai envoyés. journal:2026-10-02-03

J'ai fait une diffusion en direct avec Pedro A. Aranda Gutierrez et Protesilaos sur l'export d'Org Mode vers LaTeX avec des polices de secours. journal:2026-10-02-04

J'ai fait une autre diffusion en direct où j'ai mis à jour le programme d'EmacsConf et j'ai partagé mes fonctions pour sauter à une ligne specifiée, copier des symboles, et contrôler Emacs par commande vocale. journal:2026-10-02-05

J'ai emmené ma fille au cours de gymnastique. journal:2026-10-02-07

samedi 3

Sur Minecraft, mon mari a commencé l'extraction minière pour des diamants. Ma fille a fini la construction du bâtiment pour les villageois. Nous avons échangé des émerauds avec le paysan pour recevoir du pain. Une fois que nous les avons donnés aux villageois, ils se reproduisent. Ma fille avait installé un mod de jeu qui donne des lignes parlées aux villageois, donc nous sommes bien amusés par leurs dialogues. journal:2026-10-03-03

Selon notre nouvelle habitude, ma fille et moi sommes allées au parc d'attractions pour la neuvième fois cette année. Nos abonnements de saison sont donc bien rentabilisés. Nous avons essayé d'autres manèges que les montagnes russes, tels que la Drop Tower, Knightmares et la Shockwave. Ces manèges étaient moins fréquentés que les montagnes russes, ce qui nous a permis de faire les manèges avec très peu d'attente. Nous avons aussi fait les montagnes russes Taxi Jam et le Leviathan. Taxi Jam est la plus petite montagne russe au parc. C'est conçu pour les enfants, mais il y avait aussi des adultes et des jeunes qui ont voulu la faire. Le Léviathan est la plus grande montagne russe au Canada. Heureusement, l'attente était de seulement 30 minutes (bien prédit par leur application cette fois ; je l'ai vérifié avec le chronomètre de ma montre). Nous avons travaillé sur beaucoup de ses devoirs en attendant dans la file. Nous avons aussi essayé la nouvelle application web que j'ai générée avec Claude le matin pour reproduire le jeu Heads Up. Il fallait adapter l'idée parce que mon accéléromètre est cassé, donc nous avons ajouté des gros boutons. Nous avons aussi ajouté des catégories adaptées à nos propres centres d'intérêt tels que Donjons et Dragons. Ça a bien marché et nous nous sommes bien amusées. Nous avons regardé le spectacle Monster Bash et ma fille l'a trouvé drole. Avant de rentrer, nous avons acheté des fraises confites. journal:2026-10-03-01

Sur le chemin du retour, j'ai écouté à nouveau une partie de ma conversation avec Richard Bonichon et Prot. J'ai aussi écrit un brouillon qui décrit les commandes vocales que je veux présenter cette semaine. journal:2026-10-03-02

dimanche 4

J'ai modifié mon paquet subed pour faciliter l'ajout d'un commentaire, ce qui me permet de définir des chapitres par commande vocale. journal:2026-10-04-01

J'ai oublié la soumission de Corwin pour EmacsConf. journal:2026-10-04-07

Nous avons essayé de cuire les graines de tournesol que ma fille avait récoltées de notre jardin de devant. Ma fille les a jugées délicieuses. journal:2026-10-04-02

Nous sommes allés au magasin de cadeaux et ma fille a choisi un jouet antistress pour elle-même pour se faire plaisir ce mois-ci. Ce jouet comprend trois balles lestées avec des aimants. journal:2026-10-04-05

Nous avons mangé des pêches en conserve et des pêches séchées parce que nous avions fini tous les fruits frais chez nous. journal:2026-10-04-08

Ma fille et moi avons rangé ses tiroirs pour enlever les vêtements qui ne lui vont plus ou qui sont mal adaptés à la saison. journal:2026-10-04-06

Il faisait froid et nous étions fatigués. Nous avons donc mangé du congee au bœuf salé et au poisson. C'était le plat parfait pour ce genre de temps. journal:2026-10-04-03

Sur Minecraft, nous sommes partis à l'aventure pour chercher du fer. Nous n'avions pas autorisé les triches dans ce monde, donc nous devions faire attention pour éviter de perdre nos items. journal:2026-10-04-04

View Org source for this post

Visualizing the balance of speaking in my French conversations with tutors

| french, python, learning, visualization

I've been studying French since November 2025 because I want to help my daughter learn it. After about eight months of taking pronunciation tutoring sessions once or twice a week and writing my journal in French, I decided to try conversation-focused tutoring sessions in June 2026. These sessions help me get used to talking and identify the gaps in my active vocabulary.

These tutoring conversations feel quite different from other conversations I have. In real-life conversations with my husband and daughter, we usually leave a lot of space between sentences. When I'm hanging out with other people, I prefer to listen more than I talk. Tutoring sessions are different. I am investing time and money in practising talking. I can get a lot of listening practice from YouTube, so it's less about listening to people, although I still want to practice asking follow-up questions and connecting what the tutor says with my personal experiences.

In those conversation classes with French tutors, I want to get a sense of how much I'm actually talking. I was motivated to visualize this after some sessions with a tutor who tended to go into lecture mode. I found myself needing to be a bit firmer about directing the conversation instead of slipping into a more passive role.

I use OBS to record my sessions (with the tutor's permission, of course). I've configured it so that my microphone goes on one track and my computer audio goes on another track, which makes it easy to analyze my side of the conversation without needing to use speaker diarization.

I used Google Gemini to generate a script that uses Silero voice activity detection to analyze my side of the multi-track MKV recording.

Here's the file it generated. file:///home/sacha/proj/speech/vad-file.py

I call it with:

for FILE in ~/recordings/*conversation*.mkv; do if [ ! -f "$FILE-vad.txt" ]; then python vad-file.py -f "$FILE" -t 2 sacha 3 tutor -s 500 -o "$FILE-vad.txt"; fi; done

Then I can make a table analyzing different recordings:

(sacha-vad-tutor-make-table (directory-files sacha-recordings-dir t "-vad\\.txt\\'"))

Definition: sacha-vad-tutor-make-table

The green bars represent the time that I spent speaking, and the black bars usually correspond to when the tutor is speaking.

Date Tutor Minutes % Visualization
2026-06-05 R 26 58%
2026-06-12 R 25 59%
2026-06-16 R 26 59%
2026-06-26 R 29 65%
2026-07-17 R 30 65%
2026-07-31 R 28 59%
2026-08-04 R 34 72%
2026-08-14 R 30 70%
2026-08-21 R 28 62%
2026-09-03 R 32 69%
2026-09-08 C 18 55%
2026-09-18 R 26 61%
2026-09-25 I 21 36%
2026-09-29 I 32 58%
2026-10-09 R 33 69%

One of the things I like about my regular tutor is that he has a technical background (uses Linux and Neovim, experiments with AI), so I enjoy talking about projects and ideas with him. In my conversation classes, he does a good job of asking follow-up questions, suggesting things I could try, and correcting or filling in the occasional thing I'm trying to find a word for in French.

For fun, I decided to make a real-time display of this too: Using Emacs to visualize speaking and target words/phrases in real-ish time. I'm usually too focused on figuring out what to say in French to actually pay attention to that during the conversation itself, but maybe it'll be handy someday.

My next steps could include analyzing the other side of the conversation to find out my average time to respond. People usually start speaking within 200 milliseconds of the end of the other speaker's turn. I take a lot longer because I'm still formulating my thoughts and finding words, but it might be interesting to track my progress.

View Org source for this post

2026-10-12 Emacs news

| emacs, emacs-news

People have been having lots of fun playing Emacs Golf over in r/emacs. Check out the "Fun" category for some examples of people comparing how few keystrokes they need in order to get something done.

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

Continuing my experiment with (semi-)retirement

| experiment, life

Winding down consulting

I've been working with the same consulting client since February 2012, when I decided to start this experiment with semi-retirement. We figured out an arrangement that worked out really well. People came up with lots of ideas and I picked whichever ones I wanted to implement. I loved being able to choose when to work and what to work on. Sometimes that meant sketching out a quick prototype while the kiddo was napping, waking up early in order to get something done, or paying a babysitter in the hopes of getting a good chunk of focus time. (A+ often didn't like having a babysitter.) It turned out that I'm pretty good at coming up with prototypes and working around the limitations of systems. I appreciated being able to let other people take care of meetings and making things production-ready. It's been fun having the external validation of solving people's problems, and I've enjoyed watching my work take root and become part of the way that people do things.

My consulting contract wraps up at the end of this month, and I'm satisfied by how everything has worked out. The clients are moving away from having contractors to focusing on full-time employees, which makes perfect sense. I am very part-time, and I like it that way. Over the last two years, I've averaged about one to two hours of consulting per week as I've trusted myself with more self-directed time, so my day-to-day routines aren't going to change much. Even though the kiddo is ten and is reasonably well settled into virtual grade 5, and my husband retired in Dec 2024 and can therefore be around if she needs help, I don't want to ramp up my work.

consulting-hours-per-week.png

It did feel really nice to have clear requests, get things done, check things off lists, and solve people's problems. I learned that I tend to be more of a follower than a leader; I don't usually come up with generally-useful ideas myself, but I might be inspired by someone else's question or idea. Fortunately the low-energy days of pregnancy and parenting helped me learn a little more about adjusting to not always getting that kind of clearly-visible progress and working on my own things.

My initial five-year experiment with semi-retirement was interrupted in year four by parenting, but I think my time has now stabilized enough that we can probably consider the experiment resumed. As this consulting part winds down, I think I'll focus on my own interests instead of looking for a hustle. If I'm crunching my numbers correctly, our savings and investments can probably support our lifestyle just fine. My husband and I are both frugal by nature. If the markets crash significantly, the world has bigger problems and we'll need to figure out how to adapt anyway. In the meantime, I can sit out all the thrashing and carnage around artificial intelligence and large language models.

Retirement

I used to smooth over the initial awkwardness of the "So, what do you do?" small-talk question with saying, "Oh yeah, I do some consulting" or "These days, I'm focusing on my kiddo," but I guess I'm growing out of that phase. My real preoccupations tend to be too niche for conversation with, say, random French tutors: "I tinker with Emacs for fun. It's a text editor. Umm, that's an application you could use to write words or programs." It might be time to try out saying, "I like to program and write. Sometimes I draw." Or maybe I can get away with: "Finance, pretty boring, can't talk about it much." Depending on how energetic I feel, I can usually nudge the conversation into more interesting directions by asking the other person questions, anyway.

My husband has been in proper retirement mode for almost two years. I don't think he really does this small talk thing either. He enjoys volunteering with Bike Brigade, doing more coding and electronics, reading lots of books, and improving his health. Maybe I'll switch into proper retirement too. I'd like to get better at taking care of my health, and goodness knows I have a never-ending list of ideas I want to explore. It's a real privilege to be able to share what I'm working on and learning for free instead of joining the crowd of people trying to get other people to pay for or subscribe to vibe-coded apps and services.

Parenting

As I mentioned, the kiddo is 10, so this is an especially good opportunity to focus on spending time with her. I think she might be twice-exceptional. We're starting to see some areas where she struggles with things that might not be easily explained by her giftedness, like sensory sensitivities, emotional regulation, and social interaction. As we dig into it, I'm finding things on the self-assessments that apply to me too. For example, statement 41 on the Autism Spectrum Quotient: "I like to collect information about categories of things (e.g., types of cars, birds, trains, plants)." <gestures at Emacs News> I tend to score 40+ out of 50 on the whole self-assessment, with a threshold of 33 suggesting significant traits of autism, so yeah… Anyway, we'll see what the public school and public health systems might be able to help her with, but I'm not counting on much. Funding is limited, and lots of kids have a much harder time. For me, I don't need accommodations for work or school. Private assessments are expensive, and there's not really that much one can do for some things aside from trying to take better care of yourself, which I can do even without a formal diagnosis. (Earbuds for noisy environments, and knowing that I may need lots of quiet time; delegating social stuff to other people; not worrying too much about the kiddo while she doesn't feel like focusing on social skills - when she wants to, we can look into that.) So it'll probably help a lot to spend time figuring ourselves out, if the standard ways of doing things might not work so well.

Besides, it's fun to spend time together. We've been making the most of our season passes to the amusement park, and we've also been playing a lot of Minecraft together. She likes it when I play the same games she does. I'm finding ways to get more out of even the solo games we play side-by-side, like Pokémon. If I configure my controller to be usable with one hand, I can use the other to lift small weights, and I can listen to and repeat my French practice audio files. I won't have a kid forever, after all. My husband won't be around forever either. Good to make the most of the time we have.

I wonder what it'll be like for the kiddo to grow up seeing us both follow our own rhythms. I hope it will be good for her, despite the friction with school and other regimented systems. As I write this, she's sleeping in. I've told the school she'll probably be absent for the day. She might attend in the afternoon, or she might do her work asynchronously. (Update: She joined the afternoon class.) She tells me many of her classmates don't do the work, so the teacher spends quite a bit of time reminding people. I can see why she might not value class time as much. Having developed a keen appreciation of the difference being well-rested makes, I am okay with her experimenting with finding her own rhythms and seeing what kind of feedback she gets from her decisions. It doesn't make a ton of sense for me to say: you need to comply with school in order to get a good job. It's better to figure out something that works for you sustainably. A regular job might not be a good fit for her anyway. At least she knows she can figure out an alternative, and she can see the kinds of choices that help make that possible.

One might argue that this sort of life is too soft, too comfortable. It wastes "potential" and doesn't develop grit or persistence. I know that I can extend myself for things that matter to me, like this whole adventure into parenting. In general, though, I've greatly appreciated having plenty of slack in my life, allowing me to deal with things with less stress and more equanimity than otherwise. It feels to me better to start from a small, solid base than to feel overextended. As much as I can, I'd like to also help the kiddo figure herself out. Better to be in tune with yourself than to burn out chasing other people's priorities. Besides, it'll be interesting to see how much we can hack the system to work around its limitations.

My own interests

I'd love to keep tinkering around with Emacs, perhaps livestreaming so that people can ask questions along the way. I'd like to write about and package up my experiments so that other people can build on the ideas. I've been having lots of fun challenging my brain with French, too. In addition to the satisfaction of feeling more things click in my brain and the delight of helping the kiddo explore it outside school, I get even more reasons to play with my Emacs setup and my workflows. (Not that I needed the excuse, but still.)

So: a slow life, a "let's try things and see" sort of life, where we can experiment with things that might not yield immediate results, where it might not even be so much about getting answers but more about learning how to ask questions and listen. What could this look like? I think I might try a year-long experiment with proper retirement, zero consulting: spending time on the things that matter to me while funding expenses from savings and investments.

Time to see if my Ledger record-keeping will pay off when it comes to helping me calculate adjusted cost basis and capital gains. Let's see if I can file our taxes properly. There's paperwork, but it's probably manageable. (I still feel a sense of satisfaction from the time the Canada Revenue Agency asked about the medical expenses we claimed one year and I sent back a PDF with a proper table of contents that linked to each receipt, yay LaTeX.)

The intrinsic fun of learning things and trying things out should be plenty to keep me busy, and there's always helping out with the Emacs community or volunteering with Bike Brigade. We don't need to figure out grand, universal truths. It's enough to keep figuring out what might work in this particular moment for our peculiar selves, and perhaps share those notes in case they happen to resonate with someone.

Do I want to set up a Github Sponsors page, Patreon community, or Liberapay tip jar, like many cool Emacs people on the Internet? Not really, not for now. Times are rough for a lot of people, and I'm in a luckier position than most. I think I have enough for most of the things I want, and I like to consider those things carefully anyway. If people do want to share something, time would be amazing. I'm slowly getting the hang of paying people to help me learn, as in the case of French tutors for pronunciation and conversation and with Prot's Emacs coaching as a way of adding another perspective to some of my livestreams. I'm sure there are all sorts of things I could learn from people or work with people on.

Let's see what this retirement thing is like!

View Org source for this post

Des commandes vocales pour ajouter des tâches sur Emacs

| emacs, org, speech, speech-recognition, french

Liens :

0:00 Introduction

Je veux vous présenter ma petite expérience de la reconnaissance vocale pour dicter dans Emacs et pour les contrôler. J'utilise le modèle Orukeet pour la reconnaissance vocale parce qu'il est assez rapide et assez précis. Je l'intègre dans Emacs en utilisant mes bibliothèques sachac/whisper.el et speech-input. J'ai récemment créé des commandes vocales pour ajouter des éléments à ma liste de tâches. C'est étonnement utile parce que ça réduit la charge cognitive liée à la mémorisation des raccourcis clavier. Je vais vous montrer cela.

0:37 Ajouter des tâches

image from video Je presse la touche zéro du pavé numérique avant et après avoir dit « Rappelle-moi demain de préparer une présentation pour Emacs Paris sur les commandes vocales. » La fonction crée une tâche dans ma boîte de réception. La tâche est planifiée pour demain. Il y a un lien vers l'enregistrement

1:09 D'autres dates

image from video « Rappelez-moi demain de préparer une présentation pour Emacs Paris sur les commandes vocales », la capture d'écran et le contexte de tâche. Ça m'aident si j'ai été mal comprise ou si j'ai besoin de contexte. De la même manière, je peux dire « Rappelle-moi la semaine prochaine de faire la paperasse pour l'école. » « Rappelle-moi aujourd'hui de créer une vidéo sur mes commandes vocales. »

1:59 Basculer à une tâche

image from video Je peux aussi créer une tâche et déclencher le suivi du temps (ou clock in). Par exemple, je peux dire « Commence à implémenter la génération des questions. »

2:15 Retrouver le fil de mes pensées

image from video Si je perds le fil de mes pensées : « j'en étais où ? » Ça m'affiche les tâches récentes.

2:27 Je faisais quoi ?

image from video « Je faisais quoi ? » Ça bascule sur la tâche actuelle. Voilà des commandes vocales pour ajouter des tâches.

View Org source for this post

Emacs Carnival Oct 2026: Exploring keys and voice input in Emacs

| emacs, speech-recognition, speech

Making more use of the keyboard

October's Emacs Carnival theme "Are keyboards and M-x here to stay?" is a good nudge to think about the voice interface I've been experimenting with and where I want to take it.

Like Chris Maiorana, I love the way Emacs's M-x (execute-extended-command) allows me to call any command by name. It means that I can call commands or come up with my own commands without worrying about assigning them to keyboard shortcuts or making sense of where to place them in a hierarchical menu. With flexible completion via the vertico and orderless packages, I don’t even need to type in the full name of the command or remember the words in the right order. If I still can’t remember the command, it’s easy enough to use defalias to make my own nickname for it.

2026-10-06_11-04-07.png
Figure 1: I can filter M-x to show just my custom commands

I tend to limit keyboard shortcuts to only my most common commands. Everything else is hard to practise enough so that it sticks in my muscle memory. Even with marginalia showing me keybindings when I use M-x and which-key showing me hints when I pause, it doesn’t sink in.

2026-10-06_11-05-00.png
Figure 2: Marginalia shows keybindings

Maybe I could use key-quiz to drill a small number of keyboard shortcuts I’m actively working on learning, or modify help-quick-sections to show me reminders when I use C-h C-q (help-quick-toggle).

Getting one keyboard shortcut to do the right thing in different contexts is great because it means fewer shortcuts to memorize. Sometimes I can do that by binding the right functions to different mode maps with bind-key, like the way my sacha-org-insert-link-dwim uses the right link syntax in Markdown or OddMuse too. I also like the context-sensitive keyboard shortcuts I get from Embark. I find myself using a handful of those regularly, and C-. (embark-act) starts a bunch of shortcuts across different types of things: i to insert, w to copy, and so on. For example, I know C-x C-f (find-file) will let me select a file to open. Instead of opening it, I can use C-. i to insert the filename into the buffer.

Simpler keyboard shortcuts help. I rarely use the numpad on my laptop keyboard, so those keys can be mapped to functions that don't need to be followed by lots of typing. For example, 0 is a large button. I use it to start and stop speech recognition. Actually, I bind f9 in Emacs to sacha-whisper-run-at-point, and I bind numpad-0 as a keyboard shortcut in KDE (my window manager) instead of just in Emacs so that it's available globally. I'm still on X11 instead of Wayland. The shell script xdotool-emacs focuses Emacs briefly and sends the keys, and the shortcut is set to /home/sacha/bin/xdotool-emacs key --clearmodifiers F9. I do that instead of using emacsclient because emacsclient starts a different buffer, and I might want to keep the context. My sacha-whisper-maybe-type function detects if Emacs is not the focused application and types the text if it isn't, which means I can use it as keyboard input for oher applications while still taking advantage of the corrections and commands I've defined in Emacs. An easy-to-press global shortcut means I can use it from anywhere, even when my foreground app is the browser or another application.

An additional modifier opens up more possibilities for keyboard shortcuts near the regular typing position. My Windows key acts like a Super modifier. I've set it up as a one-shot modifier with Kanata so that I can either hold it down to access more shortcuts or tap it to treat it like a leader key.

There are quite a few commands for which neither M-x nor keyboard shortcuts feel like the right fit: things I want to do faster or more fluidly than M-x, but which I don't do often enough to memorize a good shortcut for. I've been experimenting a bit with voice input, and I really like it.

Some reasons why I'm curious about speech recognition

  • I could use more practice speaking, since I don't talk to a lot of people in real life.
    • English: Videos and livestreams can be a way to share interesting things and start conversations. I want to organize my thoughts, improve my breathing, and reduce my stutters and false starts. That will make the audio more pleasant to listen to. Dictating to my computer gives me more practice. It also makes things easier to livestream if I can reuse my speech as input.
    • French: I like challenging my brain and I want to help the kiddo learn it too. Improving my speaking will also make it easier for me to listen to myself and close the feedback loop. Maybe I can even use speech recognition to give me feedback and increase motivation while speaking, like keeping track of words or phrases I want to use. When I use them in conversation, the buffer can cross them off the list, which lets me focus on the words remaining.

      2026-10-07_07-33-03.png
      Figure 3: Screenshot showing balance and target words
  • Dictation can capture ideas quickly, with more natural wording, especially since typing sometimes gets messed up on my computer.
  • Voice interfaces might let me reduce some kinds of task friction or work hands-free. For example, I like setting timers by voice on my phone while cooking. It might be interesting to be able to dictate a draft while knitting or hand-sewing.
  • Brain fog is something I will likely need to deal with. I got a preview of it thanks to the sleep deprivation of early parenting. I know that brain fog is probably going to happen around menopause, it will probably also be part of aging, and it's a common symptom of long COVID which I might still end up getting despite my precautions. I could get into an accident and hurt my hands. I like Emacs and I want to be able to keep using it even if cognitive or physical changes make it harder to remember or use lots of keyboard shortcuts. If I start playing around with this now, the capabilities and the habits will be there when I need it.
  • Other people are already dealing with those kinds of physical or cognitive constraints, so things I figure out might be useful for other people.
  • A global shortcut for voice notes and commands let me quickly dash off a reminder before I take off. I don't have to find Emacs and go through my Org capture templates.
  • It's fun. I like saying "Prepare for take-off" and having Emacs get ready for a spontaneous stream by updating YouTube using my current Org Mode subtree title and description. I like seeing if my dictionary of substitutions can fix the commonly misrecognized words. Saying "I command you to…" to get Emacs to do a M-x command is totally inefficient but also playful, and silly things stick in my brain better thanks to the mnemonic advantage of humor. My brain likes that kind of novelty.

For context, I use Linux on a Lenovo P52 laptop (manufactured 2018, 64 GB RAM but hardly any GPU), and I have an Android phone. I like writing regular text, Emacs Lisp, and some Python and JavaScript. I'm slowly starting to have more focused time now that the kiddo is in virtual school, although it's still broken up by recess and lunch. I will probably have an increasing amount of focused time over the next few years.

Quick benefits from voice commands

I love the way voice reduces context shifts. For example, some of my most-used voice commands are:

  • remind me [today/tomorrow/next week/next month/someday] to…: capture and schedule the task; include an Org link and screenshot for context, and link to the audio recording in case the text was misrecognized

    2026-10-07_10-00-55.png
    Figure 4: Captured tasks have an audio recording, a screenshot, and a link
  • switch to…: capture the task and immediately clock into it
  • what was I doing?: jump to the current task
  • where was I?: show recently-clocked tasks

    2026-10-07_10-02-00.png
    Figure 5: This is the same as C-u M-x org-clock-goto

I don't have to think about which keys I've configured in org-capture-templates. I just use a natural phrase.

I've defined "open …" to open things from my bookmarked links stored in an Org file. Because my numpad-0 keybinding works from anywhere, I can even use it to open sites or capture notes while looking at my browser.

Because Emacs can interact with lots of other applications, I can even experiment with controlling those applications by voice. For example, I use obs-websocket.el to control OBS for streaming or recording a video. Using "Let's go live" and "Over and out" to start or stop streaming or "Start recording" and "Stop recording" to manage the recording means I can do that without needing to find the right window. I stream with a 10-second delay so that there's a little bit of time to catch any private information that's been accidentally shared on the screen. My new "panic" command lets me discard that delay and interrupt the stream by using an OBS keyboard shortcut to kill the stream immediately, even if I'm probably not going to remember that exact keyboard shortcut when I'm stressed. It should be a little easier to remember that than a keyboard shortcut I hardly ever use or a menu item that is a really small target (and my Emacs might not even be my foreground app).

For regular dictation, I'm experimenting with having it automatically place each sentence on a new line or list item. This makes it easy to reorganize in Org. I can refill it into a paragraph when I'm done.

I still have to deal with the limits of my memory, of course. I have a "what can I say" command that opens up an Org file with my notes. I keep this as a manual note instead of just dumping the contents of my sacha-whisper-commands variable so that I can prioritize the things I'm trying to build as habits.

Here's the part of my config that sets up some speech commands.

I might want more complex grammar someday for things that regular expressions can't easily handle. That's something other projects have done a good job of figuring out.

Prior work on voice control of Emacs: Voicemacs, Emacslisten

There's been a lot of good work on voice input over the years, especially since a number of Emacs users have a hard time with or would like to avoid repetitive stress injury. In addition to the innumerable projects that let you use Whisper or similar speech recognition engines to dictate prose, there are also a few projects that use a constrained vocabulary to more reliably control Emacs. Two projects in this area are based on the Talon voice control system: Voicemacs and Emacslisten.

  • The Talon community hub defines some Emacs commands. I think it simulates keyboard input and assumes that you have flx or some other fuzzy completion matching so that input like M-x t-d-on-e runs toggle-debug-on-error, as defined in community/apps/emacs/emacs_commands.csv. I seem to be having a hard time with getting orderless to do that with orderless-flex, so I'm just going to remove that column of the CSV from my emacs_commands.csv.
  • Emacslisten uses Emacs Lisp macros to define the command vocabulary. Cursorfree defines a whole bunch of them, using hatty for voice navigation within a buffer.
  • Voicemacs defines its commands in Python in the user's Talon config (see example). It feels fairly complex, like it assumes it's being cloned as your ~/.talon/user directory. It lets Emacs and Talon communicate via RPC to send additional context. It also extends some things like dired and company to make them more voice-friendly.

I'm new to both projects, so I'm not sure if I'm reading this right, but I think I might be able to use Emacslisten to define voice commands with Emacs Lisp, and then maybe someday use Voicemacs to provide richer context for some of those commands. For example, Voicemacs can list snippets, but I'm not quite sure how to use that yet.

It looks like I can use Talon for commands and my whisper.el setup for regular dictation at the same time. The Pipewire virtual sinks help with that, I think, since the programs don't have to have an exclusive lock on the audio device. This means that I can explore the kinds of vocabularies that people have set up while also taking advantage of greater accuracy and ease of correction in my whisper dictation workflow. I think I can use the Talon process socket to turn off recognition when I want to dictate using my whisper.el setup, and then I can turn it back on afterwards.

A note about the underlying speech recognition: Talon is cross-platform and free, but not open source. Subscribing to Talon (CAD 36/month) lets you use a Conformer + Whisper hybrid engine that uses whisper-large for more accuracy, but that's a bit on the expensive side for me. Dragonfly is a free open source engine that can use Kaldi or CMU Pocket Sphinx for speech recognition, but I'm not sure what the performance is like.

Interestingly, because Talon simulates keystrokes, using the commands from the community set means keys sometimes arrive out of order on my computer. "split solo" is supposed to run M-x delete-other-windows, but it sometimes ends up sending M-x delete-other-wnidows instead. This is the same sort of annoyance that gets in the way of typing quickly on my computer. Keystrokes get processed out of order when the CPU gets busy, which is part of the reason why I find dictation to be more reliable than typing sometimes. I should fix that someday, but my attempts so far aren't working. (I wonder if it has something to do with the GPU under Linux; maybe I should drop back down to CPU only since it turns out I can't use the GPU for much anyway.) Quick fix, let's try adding a brief delay in my settings.talon:

settings():
    # Adds a small pause (in ms) between each character typed
    insert_wait = 5

    # Ensures modifiers (Shift/Ctrl) don't un-latch too fast
    key_wait = 1.0

I think Voicemacs uses a sevver and and Emacslisten uses emacsclient to communicate with Emacs directly, so they probably won't run into this keystroke problem.

It might be interesting to see if I can port some of my simpler commands over to Emacslisten or straight Talon so that they're always active. Then I can slowly work on getting used to the other commands people find useful, or incorporating them into my setup.

Imagining a hybrid approach: keyboard, voice, even mouse/stylus?

I love shifting fluidly from one form of input to another, depending on what makes sense at that moment.

For example, I often start my thoughts by drawing on a light dotted grid using Noteful on my iPad. That lets me quickly cluster ideas spatially, drawing connections and highlighting words as I figure out what I want to say.

Then I use dictation and typing to sketch the outline, jumping around as thoughts occur to me. If I'm livestreaming at the same time, that helps me demonstrate things along the way and lets me benefit from people's questions. Dictation lets me explain things to people and capture the text at the same time.

I can move around with the keyboard, but it would be really cool to use voice for quick navigation between sections of my outline using either line numbers or words. I've implemented a "line" command that lets me jump to a line using the last two digits of its line number, since I probably won't have more than 100 lines visible on the screen at a time. For example, this line is currently 119, so referring to it as "nineteen" would work. I'd like to add more operations for editing the text. I think it would be great to be able to say "kill line 33" to delete that line or "move lines 16 to 20 after 27" to have it move those lines there.

I now have a sacha-whisper-one-sentence-per-line variable that splits up dictation if non-nil, so we'll see where those take me. Orukeet does a reasonable job of punctuating my sentences if I don't pause too much between phrases. My M-q re-wraps paragraphs between one sentence per line, one paragraph per line, and wrapping to fill-column, and it's probably the sort of thing that more voice commands would make even more convenient.

Maybe I can even use that spatial sense of a layout by touching buttons in an Emacs-served webpage on my tablet to direct my dictation to different places. Org-draw uses a similar Emacs web server approach to let you draw on another device and embed the results into an Org file, and there are also simpler HTTP servers (web-server, simple-httpd), so it's probably quite doable. I'm imagining something that takes the top-level items from my current nested list and makes them nice big buttons, so I could have something like "Introduction", "Keyboard", "Reasons", "Experiment", "Hybrid", and "Conclusion", and clicking on those could trigger a POST request to the server inside Emacs which moves my point accordingly. I might even have buttons for various commands. It sounds pretty straightforward to implement.

When my brain comes up with lots of interesting ideas, my org-capture voice commands let me choose which rabbit holes to postpone and which ones I want to dive into.

If I get interrupted by life, I can write on my phone while waiting for the kiddo. Orgzly Revived and Syncthing let me pick up where I left off.

As I get thoughts down, I can use keyboard shortcuts to rearrange things and then convert the list of sentences into paragraphs. I can easily turn text into links using my favourites.

I can imagine combining the audio clips from the text properties with the videos and screenshots to create a quick narrated clip that I can edit (using Emacs, of course), or publishing the narration so that people can listen to it if they like.

This combination of drawing + audio + text works better for me than any of those components alone. Visuals give me anchors for my thoughts. Audio helps with the braindump. Text permits precise, searchable details we can copy. Like George Jones, I find they all have their uses and their strengths.

When I try to dictate longer sections without that kind of visual support, I often lose my train of thought. I want to see where I've been and where I want to go. I don't trust LLMs to clean up that kind of rough transcript; the output doesn't quite resonate with me. I like keeping speech recognition constrained to sentences I can immediately check or reorganize, and I have the recordings as text properties so I can go back to that segment if I need to (at least during the current editing section). That way, I'm never left scratching my head and wondering what I meant.

I love that local speech recognition is getting better and better. I'm curious about whether small language models can do better turn detection to help compensate for my intra-phrase delays when I'm thinking of a word, and whether decision models can automatically classify a sentence into its place in my outline. I think it would be cool if embeddings or other language models could suggest related words or resources as I go along. Lots of interesting possibilities even with the technology that's already available.

Thanks to George Jones for hosting this month's Emacs Carnival on input alternatives. Check out the page for more posts!

View Org source for this post

Using Emacs to visualize speaking and target words/phrases in real-ish time

| audio, speech-recognition, emacs, learning, visualization
2026-10-07_07-33-03.png
Figure 1: Screenshot showing my list of target phrases and which ones have been recognized

When I'm working with a tutor to improve my French, I sometimes need to push myself to provide more details when I'm sharing and ask more follow-up questions in response to what they're saying, instead of falling back to « D'accord » and « C'est cool. » Visualizing the timeline in real-time lets me see if the turn-taking looks good. I might even need to take a more active role in controlling the conversation if I happen to be meeting a tutor who tends to go on monologues.

When I'm talking out loud on my own for practice, this might also nudge me to reduce the pauses and enrich my vocabulary by practising saying things often enough that they become more automatic.

1. Voice activity detection and speech recognition

This code uses this server vad-file.py, which I generated with Google Gemini. It emits JSONL for voice activity detection events (start/stop) and uses Orukeet to transcribe the speech once the voice activity detection notices it's finished. I also use spacy to look up token information because I want to someday colour the nouns by gender, which might allow me to correct myself in the next sentence instead of hesitating in the previous one.

code for sacha-vad-tutor
;; Report on the balance between my tutor and me
(defvar sacha-vad-tutor-process nil)
(defvar sacha-vad-tutor-dir "~/vendor/orukeet")
(defvar sacha-vad-tutor-command `(,(expand-file-name ".venv/bin/python" sacha-vad-tutor-dir)
                                  "asr.py"
                                  "--spacy-model"
                                  "fr_dep_news_trf"
                                  "-s" "300" "-t"
                                  "VirtualMicSink.monitor=sacha" "alsa_output.usb-AT_AT2040USB_202011110001-00.analog-stereo.monitor=other"))
(defvar sacha-vad-tutor-events nil "List of events as an list of alists (label start stop duration).")
(defvar sacha-vad-tutor-labels '("sacha" "other"))
(defvar sacha-vad-tutor-summary '(("sacha" . 0)
                                  ("other" . 0)))
(defvar sacha-vad-tutor-session-length (* 60 60) "Number of seconds to use for scale.")

(defun sacha-vad-tutor-update-view ()
  "Visualize the last 3 minutes and the overview of the session.
Also keep track of target phrases."
  (interactive)
  (with-current-buffer (get-buffer-create "*tutor stats*")
    (erase-buffer)
    (let* ((total (apply '+ (mapcar 'cdr sacha-vad-tutor-summary)))
           (height 30)
           (width (window-width nil t))
           (max-seconds
            (if sacha-vad-tutor-events
                (apply 'max (mapcar (lambda (o) (alist-get 'end o))
                                    sacha-vad-tutor-events))
              0))
           (threshold 180)              ; last three minutes
           (threshold-seconds (- max-seconds threshold))
           (recent
            (seq-keep
             (lambda (o)
               (when (>= (alist-get 'start o)
                         threshold-seconds)
                 (let-alist o
                   `((label . ,.label)
                     (start . ,(- .start threshold-seconds))
                     (end . ,(- .end threshold-seconds))))))
             (copy-sequence sacha-vad-tutor-events)))
           (recent-total (apply '+ (mapcar (lambda (o)
                                             (-
                                              (alist-get 'end o)
                                              (alist-get 'start o)))
                                           recent))))
      (save-excursion
        (insert
         (format "Last %d minute%s:\n"
                 (/ threshold 60)
                 (if (> (/ threshold 60) 1)
                     "s"
                   ""))
         (mapconcat
          (lambda (o)
            (format "%d%% %s\n"
                    (/ (* 100.0 (cdr o)) recent-total)
                    (car o)))
          (seq-map
           (lambda (group)
             (cons (car group)
                   (apply '+ (mapcar (lambda (o)
                                       (-
                                        (alist-get 'end o)
                                        (alist-get 'start o)))
                                     (cdr group)))))
           (seq-group-by (lambda (o) (alist-get 'label o)) recent)))
         "\n")
        (svg-insert-image
         (sacha-vad-visualize
          sacha-vad-tutor-labels
          recent
          t
          width height))
        (insert
         "\nTotal:\n"

         (mapconcat
          (lambda (o)
            (format "%d%% %s\n"
                    (/ (* 100.0 (cdr o)) total)
                    (car o)))
          sacha-vad-tutor-summary))
        (svg-insert-image
         (sacha-vad-visualize sacha-vad-tutor-labels sacha-vad-tutor-events t
                              width height
                              sacha-vad-tutor-session-length))
        (insert "\n\n"  (sacha-learn-lang-bingo-summary))))))

(defun sacha-vad-handle-end-event (data)
  "Handle the end event."
  (let ((event (list
                (cons 'label (alist-get 'label data))
                (cons 'start (- (alist-get 'timestamp data)
                                (alist-get 'duration data)
                                (or sacha-vad-tutor-start-seconds 0)))
                (cons 'end (- (alist-get 'timestamp data)
                              (or sacha-vad-tutor-start-seconds 0))))))
    (push event sacha-vad-tutor-events)
    (let-alist data
      (incf (alist-get (alist-get 'label data) sacha-vad-tutor-summary 0 nil 'string=)
            .duration))
    (sacha-vad-tutor-update-view)))

(defvar sacha-vad-event-handler-functions '(sacha-vad-speech-process) "Functions to call with the VAD/transcription events.")

(defvar sacha-vad-process-my-speech nil "Non-nil means process my speech through the whisper setup.")
(defun sacha-vad-speech-process (data)
  "Process DATA through my whisper input process if it's my speech.
Add to `sacha-vad-event-handler-functions'."
  (when (and sacha-vad-process-my-speech
             (string= (alist-get 'event data) "final")
             (string= (alist-get 'label data) sacha-vad-my-label))
    (with-current-buffer (get-buffer-create whisper--stdout-buffer-name)
      (erase-buffer)
      (insert (alist-get 'text data))
      (goto-char (point-min)))
    (whisper--handle-transcription-output nil (get-buffer-create whisper--stdout-buffer-name))))

(defun sacha-vad-tutor-filter (proc string)
  "Add and parse."
  (when (buffer-live-p (process-buffer proc))
    ;; split the string by newline?
    (let ((data (with-current-buffer (process-buffer proc)
                  (goto-char (point-max))
                  (save-excursion
                    (insert string))
                  (catch 'waiting
                    (unless (or (eobp) (looking-at "\n*$"))
                      (json-read))))))
      (run-hook-with-args 'sacha-vad-event-handler-functions data)
      (when (string= (alist-get 'event data) "end")
        (sacha-vad-handle-end-event data))
      (when (string= (alist-get 'event data) "final")
        (sacha-vad-handle-text-event data)))))

(defvar sacha-vad-last-speaker nil "Previous speaker.")

(defvar sacha-vad-transcript-buffer nil "If non-nil, add speech to this buffer.")
(defun sacha-vad-add-to-transcript (text)
  "Add TEXT to the transcript buffer."
  (when sacha-vad-transcript-buffer
    (with-current-buffer (get-buffer-create sacha-vad-transcript-buffer)
      (set (make-local-variable 'window-point-insertion-type) t)
      (save-excursion
        (goto-char (point-max))
        (insert text)))))

(defvar sacha-vad-my-label "sacha")
(defun sacha-vad-handle-text-event (data)
  (sacha-vad-add-to-transcript
   (concat
    (if (string= sacha-vad-last-speaker (alist-get 'label data))
        " "
      (concat (if (bobp) "" "\n\n") (alist-get 'label data) ": "))
    (alist-get 'text data)))
  (setq sacha-vad-last-speaker (alist-get 'label data))
  (when (string= (alist-get 'label data) sacha-vad-my-label)
    (sacha-learn-lang-notice-bingo-words (alist-get 'text data))))

(defun sacha-vad-tutor-start ()
  "Start the process."
  (interactive)
  (sacha-vad-tutor-ensure)
  (setq sacha-vad-tutor-summary '(("sacha" . 0)
                                  ("other" . 0))
        sacha-vad-tutor-events nil)
  (with-current-buffer (get-buffer-create "*Transcript*")
    (erase-buffer)
    (display-buffer (current-buffer)))
  (display-buffer (get-buffer-create "*tutor stats*")))

(defun sacha-vad-tutor-ensure ()
  "Start the process if it's not already running."
  (interactive)
  (unless (process-live-p sacha-vad-tutor-process)
    (setq sacha-vad-tutor-process-start-time (current-time))
    (setq sacha-vad-tutor-start-seconds 0)
    (let ((default-directory sacha-vad-tutor-dir))
      (setq sacha-vad-tutor-process
            (make-process
             :name "vad-tutor"
             :command sacha-vad-tutor-command
             :connection-type 'pipe
             :buffer (get-buffer-create "*vad-tutor*")
             :stderr (get-buffer-create "*vad-tutor-err*")
             :filter #'sacha-vad-tutor-filter)))))

(defun sacha-vad-tutor-stop ()
  "Stop the process."
  (interactive)
  (interrupt-process sacha-vad-tutor-process))

;;;###autoload
(defun sacha-js-parse-jsonl-file (file)
  "Parse jsonl as a list"
  (let (results)
    (with-temp-buffer
      (insert-file-contents file)
      (goto-char (point-min))
      (while (not (eobp))
        (condition-case nil
            (push (json-read) results)
          (error (goto-char (point-max))))))
    (nreverse results)))

(defvar sacha-vad-tutor-start-seconds 0 "Number of seconds to consider as the start.")
(defvar sacha-vad-tutor-process-start-time nil "Time that the process started.")

;;;###autoload
(defun sacha-vad-tutor-reset-clock ()
  "Reset the start of the session."
  (interactive)
  (setq sacha-vad-tutor-start-seconds
        (time-to-seconds (time-subtract (current-time)
                                        sacha-vad-tutor-process-start-time)))
  (mapcar (lambda (o) (setcdr o 0)) sacha-vad-tutor-summary)
  (setq sacha-vad-tutor-events nil))

(defun sacha-vad-visualize (labels list &optional output-file width height max-seconds)
  (setq height (or height 25))
  (setq width (or width (window-width nil t)))
  (let* ((svg (svg-create width height))
         (max-seconds (max
                       (or max-seconds 0)
                       (if list
                           (apply 'max (mapcar (lambda (o) (alist-get 'end o))
                                               list))
                         0)))
         (pixels-per-second (/ (* 1.0 width) max-seconds))
         (row-height (/ height (length labels))))
    (dolist (o list)
      (let-alist o
        (let ((y (* row-height (or (seq-position labels .label 'string=) -1))))
          (when (>= y 0)
            (svg-rectangle
             svg
             (* .start pixels-per-second)
             y
             (* (- .end .start) pixels-per-second)
             row-height :fill "green")))))
    (cond
     ((stringp output-file)
      (with-temp-file output-file
        (svg-print svg)))
     (output-file
      svg)
     (t
      (with-current-buffer (get-buffer-create "*vad*")
        (goto-char (point-min))
        (display-buffer (current-buffer))
        (erase-buffer)
        (svg-insert-image svg))))))

(defun sacha-vad-stats (labels list)
  "Return the balance of time talking as (label total percentage)."
  (let* ((max-seconds (apply '+ (mapcar
                                 (lambda (o) (let-alist o (- .end .start)))
                                 list)))
         (durations (mapcar (lambda (o)
                            (cons
                             (car o)
                             (mapcar (lambda (e) (let-alist e (- .end .start)))
                                     (cdr o))))
                          (seq-group-by (lambda (o) (alist-get 'label o))
                                        list))))
    (seq-map
     (lambda (label)
       (let* ((row (cdr (assoc label durations)))
              (total (apply '+ row)))
         `((label . ,label)
           (total . ,total)
           (count . ,(length row))
           (percentage . ,(and total max-seconds (> max-seconds 0) (* 100.0 (/ total max-seconds)))))))
     labels)))

;; (sacha-vad-stats '("sacha" "tutor") (seq-take (sacha-js-parse-jsonl-file "~/proj/speech/output2.log") 5))
;; (sacha-vad-stats '("sacha" "tutor") (sacha-js-parse-jsonl-file "~/proj/speech/output.log"))
;; (sacha-vad-visualize '("sacha" "tutor") (sacha-js-parse-jsonl-file "~/proj/speech/output.log"))
;; (sacha-vad-visualize '("sacha" "tutor") (seq-take (sacha-js-parse-jsonl-file "~/proj/speech/output2.log") 30))

(defun sacha-vad-tutor-group-output (files)
  (let ((grouped-files (seq-group-by
                        (lambda (o)
                          (substring (file-name-base o) 0 10))
                        files)))
    (mapcar
     (lambda (group)
       (let ((start 0))
         (cons (car (cdr group))
               (seq-reduce
                (lambda (prev file)
                  (append prev
                          (let ((max-seconds 0))
                            (prog1 (mapcar (lambda (event)
                                             (incf (alist-get 'start event) start)
                                             (incf (alist-get 'end event) start)
                                             (setq max-seconds (max (alist-get 'end event) max-seconds))
                                             event)
                                           (sacha-js-parse-jsonl-file file))
                              (setq start max-seconds)))))
                (cdr group)
                nil))))
     grouped-files)))

(defun sacha-vad-tutor-make-table (files &optional width height)
  "Make a summary table."
  (let ((labels sacha-vad-tutor-labels)
        (width (or width 500))
        (height (or height 30)))
    ;; combine data as needed
    (append
     '(("Date" "Tutor" "Minutes" "%" "Visualization") hline)
     (seq-map
      (lambda (group)
        (let* ((list (cdr group))
               (base (file-name-base (car group))))
          (if list
              (let-alist (car (sacha-vad-stats labels list))
                (let ((svg-name
                       (expand-file-name
                        (concat (file-name-base (car group)) "-" .label ".svg")
                        "~/proj/french/conversation")))
                  (sacha-vad-visualize
                   (list .label)
                   list
                   svg-name
                   width
                   height)
                  (list
                   (format-time-string "%Y-%m-%d"
                                       (seconds-to-time
                                        (sacha-filename-timestamp (car group))))
                   ;; (org-link-make-string (concat "file:" o) "file")
                   (if (string-match "-conversation-\\([^\\.]+\\)" base)
                       (s-capitalize (substring (match-string 1 base) 0 1))
                     "")

                   (round (/ .total 60))
                   (format "%d%%" .percentage)
                   (org-link-make-string (concat "file:" svg-name)
                                         (concat "file:" svg-name))
                   )))
            (list "?"))))
      (sacha-vad-tutor-group-output files))
     nil)))

2. Noticing bingo phrases

Here's the code that reminds me to use the words or expressions on my to-do list, and then keeps track of how often I've used them, prioritizes the ones that I haven't used yet, and displays these. It can work with either my Whisper dictation workflow or the VAD events server described above.

I keep the list of expressions in file:///home/sacha/proj/french/bingo.txt.

Code to notice when I've used an expression
(defvar sacha-learn-lang-bingo-word-file "~/proj/french/bingo.txt")
(defvar sacha-learn-lang-bingo-words nil "List of words.")
(defvar sacha-learn-lang-bingo-words-hash nil "Data for the counts.")

;;;###autoload
(defun sacha-learn-lang-bingo-words ()
  "Load the list of words from the file."
  (interactive)
  (setq sacha-learn-lang-bingo-words
        (seq-map (lambda (o)
                   (when (= (length o) 1)
                     (setq o (list (car o) 0)))
                   (setq o (append o (list
                                      (string-trim (replace-regexp-in-string
                                                    " *\\[.+?\\] *" " "
                                                    (car o))))))
                   o)
                 (pcsv-parse-file sacha-learn-lang-bingo-word-file)))
  sacha-learn-lang-bingo-words)

;;;###autoload
(defun sacha-learn-lang-bingo-handler (event)
  "Manually register the event."
  (interactive "e")
    (let* ((pos (posn-point (event-end event)))
           (val (get-text-property pos 'bingo)))
      (when val
        (sacha-learn-lang-notice-bingo-words val))))

(defvar-keymap sacha-learn-lang-bingo-map
  "<mouse-1>" #'sacha-learn-lang-bingo-handler)

;;;###autoload
(defun sacha-learn-lang-bingo-summary ()
  "Summarize the words to say."
  (let ((target (seq-filter (lambda (row) (= (elt row 1) 0))
                            sacha-learn-lang-bingo-words))
        (yay (seq-filter (lambda (row) (> (elt row 1) 0))
                         sacha-learn-lang-bingo-words)))
    (concat
     (if target
         (concat "Target: "
                 (mapconcat (lambda (row)
                              (propertize
                               (car row)
                               'keymap sacha-learn-lang-bingo-map
                               'bingo (car row)))
                            target
                            "; ")
                 "\n\n")
       "")
     (if yay
         (concat
          "Yay! "
          (mapconcat (lambda (row)
                       (propertize (format "%s (%s)" (car row) (elt row 1))
                                   'keymap sacha-learn-lang-bingo-map
                                   'bingo (car row)))
                     (seq-filter (lambda (row) (> (elt row 1) 0))
                                 sacha-learn-lang-bingo-words)
                     "; ")
          "\n\n")
       ""))))

;;;###autoload
(defun sacha-learn-lang-notice-bingo-words (text)
  "Look for the bingo words in TEXT and celebrate when I use them.
Add to `whisper-insert-text-at-point'."
  (when text
    (unless sacha-learn-lang-bingo-words (sacha-learn-lang-bingo-words))
    (let ((found (seq-find
                  (lambda (o)
                    (and (elt o 2)
                         (string-match (concat "\\<" (elt o 2)) text)))
                  sacha-learn-lang-bingo-words)))
      (when found
        (incf (elt found 1))
        (sacha-vad-tutor-update-view)
        (with-current-buffer (get-buffer-create "*Bingo*")
          (erase-buffer)
          (insert (sacha-learn-lang-bingo-summary))))))
  text)
This is part of my EmacsConfiguration.
View Org source for this post