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.
Progress, bit by bit! Here's a screenshot showing the Google Docs draft on one side and my web preview in the other:
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.
I want to be able to record, stream, screen share, and do speech recognition, possibly all at the same time. If I just try having those processes read directly from my microphone, I find that the audio skips. I'm on Linux, so it turns out that I can set up Pipewire with a virtual audio cable (loopback device) connecting my microphone to a virtual output (null sink) with some latency (100ms seems good) so that multiple applications listening to the null sink can get the audio packets smoothly.
I was getting a little confused connecting things to other things, though. qpwgraph was helpful for starting to understand how everything was actually connected to each other, and also for manually changing the connections on the fly.
Figure 1: qpwgraph screenshot
Like with other graphical applications, I found myself wondering: could I do this in Emacs instead? I wanted to just focus on a small set of the nodes. For example, I didn't need all of the lines connecting to the volume control apps. I also wanted the ability to focus on whichever nodes were connected to my microphone.
Unsurprisingly, there is a pipewire package in MELPA.
Figure 2: Screenshot of M-x pipewire from the pipewire package
Let's call it with C-u, which prompts for a regexp of nodes to focus on and another regexp for nodes to exclude. Then I can ignore the volume control:
Figure 4: Ignoring the volume control
I can focus on just the things that are connected to my microphone:
Figure 5: Focusing on a regular expression
This also lets me disconnect things with d (epwgraph-disconnect-logical-nodes):
Figure 6: Disconnecting a link
and connect them with c (epwgraph-connect-logical-nodes).
Figure 7: Connecting links
I don't have a fancy 5.1 sound systems, so the logic for connecting nodes just maps L and R if possible.
Most of the time I just care about the logical devices instead of the specific left and right channels, but I can toggle the display with t so that I can see specific ports:
Figure 8: Showing specific ports
and I can use C and D to work with specific ports as well.
Figure 9: Connecting specific ports
I usually just want to quickly rewire a node so that it gets its input from a specified device, which I can do with i (epwgraph-rewire-inputs-for-logical-node).
Figure 10: Animated GIF showing how to change the input for a node.
I think this will help me stay sane when I try to scale up my audio configuration to having four or five web conferences going on at the same time, possibly with streaming speech recognition.
Ideas for next steps:
I want to be able to set the left/right balance of audio, probably using pactl set-sink-volume <index> left% right%
I'd love to be able to click on the graph in order to work with it, like dragging from one box to another in order to create a connection, right-drag to disconnect, or shift-drag to rewire the inputs.
Emacs can do everything. Except the most simple thing ever as I learned after 40 years in which I
never needed it: Edit one file twice at the same time.
I can open a new Emacs "window" and re-open the file. But Emacs notices and this and shows the
file's buffer in the new window, not a new buffer.
But why? Well, when editing and SVG file, you can switch between the XML and the rendered image with
C-c C-c, but I would like to see the XML and the rendered next to each other.😀
You might think this is easy, just use M-x clone-indirect-buffer-other-window. But image-mode adds a wrinkle. It uses text properties to display the image, so even if you have two views of the same buffer thanks to clone-indirect-buffer, C-c C-c will toggle both of them. If we want to edit a file as both text and an SVG at the same time, we need to actually have two separate file buffers.
I started off by looking at how find-file works. From there, I went to find-file-noselect. Normally, find-file-no-select reuses any existing buffers visiting the same file. If it doesn't find any, it calls find-file-noselect-1. That lets me write this short function to jump straight to that step.
This code unconditionally opens a buffer visiting a file, so you could have multiple buffers, looking at the same file independently. With global-auto-revert-mode, editing the file in one buffer and saving it will result in changes in the other.
I sometimes play around with SVGs, and it might be helpful to be able to experiment with the source code of the SVG while seeing the changes refreshed automatically.
I really like how in Emacs, you can follow the trail of the functions to find out how they actually work.
Screencast demonstrating my-find-file-always
Transcript
00:00:00The problem: clone-indirect-buffer-other-window and image-mode
@HaraldKi@nrw.social said,"Emacs can do everythingexcept the most simple thing ever,as I learned after 40 yearsin which I never needed it:edit one file twice at the same time."You might think this is easy,just use M-x clone-indirect-buffer-other-window,but image mode adds a wrinkle.So let's show you how that works.I've got my test SVG here.We can say clone-indirect-buffer-other-window.But if I use C-c C-c,you'll notice that both of the windows change.That's because image mode uses text propertiesinstead of some other kind of display.I mean, it's the same buffer that's being reusedfor the clone. So that doesn't work.
00:00:48A quick tour of find-file
What I did was I looked at how find-file works.And then from there, I went to find-file-noselect.So this is find-file over here.If you look at the source code,you'll see how it uses find-file...It's a very short function, actually.It uses find-file-noselect.And find-file-noselect reuses a buffer if it can.Let's show you where we're looking for this. Ah, yes.So here's another buffer here.And what we want to do iswe want to open a new file buffer no matter what.The way that find-file-noselect actually worksis it calls this find-file-noselect1.And by taking a look at how it figured outthe raw file and the true name and the numberto send to it, I was able to writethis short function, my-find-file-always,and a my-clone-file-other-window.
00:01:46Demonstration of my-find-file-always
So if I say my-find-file-always,then it will always open that file,even if it's already open elsewhere.
00:01:57Cloning it into the other window
Let's show you how it workswhen I clone it in the other window.All right, so if I switch this one to text mode,I can make changes to it. More stuff goes here.And as you can see, that added this over here.I have global-auto-revert mode on,so it just refreshes automatically.So yeah, that's this function.
Looking for something to write about? Christian Tietze is hosting the January Emacs Carnival on the theme "This Year, I'll…". Check out last month's carnival on The People of Emacs for other entries.
: Major change: I switched to my fork of natrys/whisper.el so that I can specify functions that change the window configuration etc.
: Change main function to my-whisper-run, use seq-reduce to go through the functions.
: Added code for automatically capturing screenshots, saving text, working with a list of functions.
: Added demo, fixed some bugs.
: Added note about difference from MELPA package, fixed :vc
I want to get my thoughts into the computer quickly, and talking might be a good way to do some of that. OpenAI Whisper is reasonably good at recognizing my speech now and whisper.el gives me a convenient way to call whisper.cpp from Emacs with a single keybinding. (Note: This is not the same whisper package as the one on MELPA.) Here is how I have it set up for reasonable performance on my Lenovo P52 with just the CPU, no GPU.
I've bound <f9> to the command whisper-run. I press <f9> to start recording, talk, and then press <f9> to stop recording. By default, it inserts the text into the buffer at the current point. I've set whisper-return-cursor-to-start to nil so that I can keep going.
(use-package whisper
:vc (:url"https://github.com/natrys/whisper.el")
:load-path"~/vendor/whisper.el":config
(setq whisper--mode-line-recording-indicator "⏺")
(setq whisper-quantize "q4_0")
(setq whisper-install-directory "~/vendor")
(setq whisper--install-path (concat
(expand-file-name (file-name-as-directory whisper-install-directory))
"whisper.cpp/"))
;; Get it running with whisper-server-mode set to nil first before you switch to 'local.;; If you change models,;; (whisper-install-whispercpp (whisper--check-install-and-run nil "whisper-start"))
(setq whisper-server-mode 'local)
(setq whisper-model "base")
(setq whisper-return-cursor-to-start nil)
;(setq whisper--ffmpeg-input-device "alsa_input.usb-Blue_Microphones_Yeti_Stereo_Microphone_REV8-00.analog-stereo")
(setq whisper--ffmpeg-input-device "VirtualMicSink.monitor")
(setq whisper-language "en")
(setq whisper-recording-timeout 3000)
(setq whisper-before-transcription-hook nil)
(setq whisper-use-threads (1- (num-processors)))
(setq whisper-transcription-buffer-name-function 'whisper--simple-transcription-buffer-name)
(add-hook 'whisper-after-transcription-hook'my-subed-fix-common-errors-from-start -100)
:bind
(("<f9>" . whisper-run)
("C-<f9>" . my-whisper-run)
("S-<f9>" . my-whisper-replay)
("M-<f9>" . my-whisper-toggle-language)))
Let's see if we can process "Computer remind me to…":
The technology isn't quite there yet to do real-time audio transcription so that I can see what it understands while I'm saying things, but that might be distracting anyway. If I do it in short segments, it might still be okay. I can replay the most recently recorded snippet in case it's missed something and I've forgotten what I just said.
(defunmy-whisper-toggle-language ()
"Set the language explicitly, since sometimes auto doesn't figure out the right one."
(interactive)
(setq whisper-language (if (string= whisper-language "en") "fr""en"))
;; If using a server, we need to restart for the language
(when (process-live-p whisper--server-process) (kill-process whisper--server-process))
(message "%s" whisper-language))
I could use this with org-capture, but that's a lot of keystrokes. My shortcut for org-capture is C-c r. I need to press at least one key to set the template, <f9> to start recording, <f9> to stop recording, and C-c C-c to save it. I want to be able to capture notes to my currently clocked in task without having an Org capture buffer interrupt my display.
To clock in, I can use C-c C-x i or my !speed command. Bonus: the modeline displays the current task to keep me on track, and I can use org-clock-goto (which I've bound to C-c j) to jump to it.
Then, when I'm looking at something else and I want to record a note, I can press <f9> to start the recording, and then C-<f9> to save it to my currently clocked task along with a link to whatever I'm looking at. (Update: Ooh, now I can save a screenshot too.)
(defunmy-whisper-reset (text)
(setq my-whisper-skip-annotation nil)
(remove-hook 'whisper-insert-text-at-point#'my-whisper-org-save-to-clocked-task)
text)
;; Only works with my tweaks to whisper.el;; https://github.com/sachac/whisper.el/tree/whisper-insert-text-at-point-function
(with-eval-after-load'whisper
(setq whisper-insert-text-at-point
'(my-whisper-handle-commands
my-whisper-save-text
my-whisper-save-to-file
my-whisper-maybe-type
my-whisper-insert
my-whisper-reset)))
(defvarmy-whisper-last-annotation nil "Last annotation so we can skip duplicates.")
(defvarmy-whisper-skip-annotation nil)
(defvarmy-whisper-target-markers nil "List of markers to send text to.")
(defunmy-whisper-insert (text)
(let ((markers
(cond
((null my-whisper-target-markers)
(list whisper--marker)) ; current point where whisper was started
((listp my-whisper-target-markers)
my-whisper-target-markers)
((markerp my-whisper-target-markers)
(list my-whisper-target-markers))))
(orig-point (point))
(orig-buffer (current-buffer)))
(when text
(mapcar (lambda (marker)
(with-current-buffer (marker-buffer marker)
(save-restriction
(widen)
(when (markerp marker) (goto-char marker))
(when (and (derived-mode-p 'org-mode) (org-at-drawer-p))
(insert "\n"))
(whisper--insert-text
(concat
(if (looking-back "[ \t\n]\\|^")
""" ")
(string-trim text)))
;; Move the marker forward here
(move-marker marker (point)))))
markers)
(when my-whisper-target-markers
(goto-char orig-point))
nil)))
(defunmy-whisper-maybe-type (text)
(when text
(if (frame-focus-state)
text
(make-process :name"xdotool":command
(list "xdotool""type"
text))
nil)))
(defunmy-whisper-clear-markers ()
(interactive)
(setq my-whisper-target-markers nil))
(defunmy-whisper-use-current-point (&optional add)
(interactive (list current-prefix-arg))
(if add
(push (point-marker) my-whisper-target-markers)
(setq my-whisper-target-markers (list (point-marker)))))
(defunmy-whisper-run-at-point (&optional add)
(interactive (list current-prefix-arg))
(my-whisper-clear-markers)
(whisper-run))
(keymap-global-set "<f9>"#'my-whisper-run-at-point)
(keymap-global-set "<kp-1>"#'whisper-run)
(defunmy-whisper-jump-to-marker ()
(interactive)
(with-current-buffer (marker-buffer (car my-whisper-target-markers))
(goto-char (car my-whisper-target-markers))))
(defunmy-whisper-use-currently-clocked-task (&optional add)
(interactive (list current-prefix-arg))
(save-window-excursion
(save-restriction
(save-excursion
(org-clock-goto)
(org-end-of-meta-data)
(org-end-of-subtree)
(if add
(push (point-marker) my-whisper-target-markers)
(setq my-whisper-target-markers (list (point-marker))))))))
(defunmy-whisper-run (&optional skip-annotation)
(interactive (list current-prefix-arg))
(require'whisper)
(add-hook 'whisper-insert-text-at-point#'my-whisper-org-save-to-clocked-task -10)
(whisper-run)
(when skip-annotation
(setq my-whisper-skip-annotation t)))
(defunmy-whisper-save-text (text)
"Save TEXT beside `whisper--temp-file'."
(when text
(let ((link (org-store-link nil)))
(with-temp-file (concat (file-name-sans-extension whisper--temp-file) ".txt")
(when link
(insert link "\n"))
(insert text)))
text))
(defunmy-whisper-org-save-to-clocked-task (text)
(when text
(save-window-excursion
(with-current-buffer (if (markerp whisper--marker) (marker-buffer whisper--marker) (current-buffer))
(when (markerp whisper--marker) (goto-char whisper--marker))
;; Take a screenshot maybe
(let* ((link (and (not my-whisper-skip-annotation)
(org-store-link nil)))
(region (and (region-active-p) (buffer-substring (region-beginning) (region-end))))
(screenshot-filename
(when (or
(null link)
(not (string= my-whisper-last-annotation link))
(not (frame-focus-state))) ; not in focus, take a screenshot
(my-screenshot-current-screen (concat (file-name-sans-extension whisper--temp-file) ".png")))))
(if (org-clocking-p)
(save-window-excursion
(save-restriction
(save-excursion
(org-clock-goto)
(org-end-of-subtree)
(unless (bolp)
(insert "\n"))
(insert "\n")
(if (and link (not (string= my-whisper-last-annotation link)))
(insert
(if screenshot-filename
(concat "(" (org-link-make-string
(concat "file:" screenshot-filename)
"screenshot") ") ")"")
link
"\n")
(when screenshot-filename
(insert (org-link-make-string
(concat "file:" screenshot-filename)
"screenshot")
"\n")))
(when region
(insert "#+begin_example\n" region "\n#+end_example\n"))
(insert text "\n")
(setq my-whisper-last-annotation link)))
(run-at-time 0.5 nil (lambda (text) (message "Added clock note: %s" text)) text))
;; No clocked task, prompt for a place to capture it
(kill-new text)
(setq org-capture-initial text)
(call-interactively 'org-capture)
;; Delay the window configuration
(let ((config (current-window-configuration)))
(run-at-time 0.5 nil
(lambda (text config)
(set-window-configuration config)
(message "Copied: %s" text))
text config))))))))
(with-eval-after-load'org
(add-hook 'org-clock-in-hook#'my-whisper-org-clear-saved-annotation))
(defunmy-whisper-org-clear-saved-annotation ()
(setq my-whisper-org-last-annotation nil))
Here's an idea for a function that saves the recognized text with a timestamp.
(defvarmy-whisper-notes"~/sync/stream/narration.org")
(defunmy-whisper-save-to-file (text)
(when text
(let ((link (org-store-link nil)))
(with-current-buffer (find-file-noselect my-whisper-notes)
(goto-char (point-max))
(insert "\n\n" (format-time-string "%H:%M ") text "\n" (if link (concat link "\n") ""))
(save-buffer)
(run-at-time 0.5 nil (lambda (text) (message "Saved to file: %s" text)) text)))
text))
I think I've just figured out my Pipewire setup so
that I can record audio in OBS while also being
able to do speech to text, without the audio
stuttering. qpwgraph was super helpful
for visualizing the Pipewire connections and fixing them.
Screencast of using whisper.el to do speech-to-text into the current buffer, clocked-in task, or other function
Transcript
00:00:00Inserting into the current buffer
Here's a quick demonstrationof using whisper.el to log notes.
00:00:13Inserting text and moving on
I can insert text into the current bufferone after the other.
00:00:31Clocking in
If I clock into a task,I can add to the end of that clocked in taskusing my custom codeby pressing C-<f9>or whatever my shortcut was.I can do that multiple times.
00:01:05Logging a note from a different file
I can do that while looking at a different file.
00:01:15I can look at an info page
I can do it looking at an info page, for example,and annotations will include a linkback to whatever I was looking at.
00:01:33Adding without an annotation (C-u)
I just added an optional argumentso that I can also capture a notewithout saving an annotation.That way, if I'm going to say a lot of thingsabout the same buffer,I don't have to have a lot of linksthat I need to edit out.