EmacsConf 2023 is less than a month away. Speakers have been uploading
videos, captioning volunteers have been editing away, and I thiiiiink
I've gotten most of the infrastructure dusted off. Exciting!
Here's where we are with regard to talk status:
Waiting for 26 talks (~550 minutes) out of 42 total. Talks received so
far:
Speakers have been really nice about keeping in touch, so I'm not too
stressed about gaps in the schedule. Captioning volunteers have been
chugging through the talks and OpenAI Whisper's gotten a bit better at
spelling things, so that's terrific too. It's so exciting!
zaeph and bandali will probably host the general track and the
development track respectively. They've done it for a number of years
now, so it'll probably be fine even if we don't have a dry run all
together since they've got limited availability. (And we can take on
new volunteers if people want to help read questions!)
My stress level is pretty manageable at this point. I can even spend
evenings playing video games with the kiddo and weekends going on
little bike adventures, so that's awesome. I'm still a little worried
about tech hiccups, but we'll probably be able to figure things out.
Next steps are:
keep processing videos and captions
make the intro videos available so that speakers can correct my
pronunciation of their names
smoothen out and document the process for last-minute submissions
One of the benefits of volunteering for or speaking at EmacsConf is
that you get early access to the talks. We upload the files to a
password-protected web server. It's nothing fancy, just a directory
that has basic authentication and a static index.html generated by
Emacs Lisp.
I organize the talks by TODO status so that people can easily see
which talks are ready to be captioned. Since captioning can take a
little while, we reserve the talks we want to caption. That way,
people don't accidentally work on a talk that someone else is already
captioning. When people e-mail me to reserve a talk, I move the talk
from TO_ASSIGN to TO_CAPTION and add their name in the
:CAPTIONER: property.
Publishing the backstage index is done by an Emacs Lisp function that
smooshes the information together and then writes the files directly
to the server using TRAMP.
emacsconf-publish-backstage-index
(defunemacsconf-publish-backstage-index (&optional filename)
(interactive)
(setq filename (or filename (expand-file-name "index.html" emacsconf-backstage-dir)))
(let ((info (or emacsconf-schedule-draft (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
(emacsconf-main-extensions (append emacsconf-main-extensions emacsconf-publish-backstage-extensions)))
(with-temp-file filename
(let* ((talks
(mapcar
(lambda (o) (append
(list :captions-edited t) o))
(seq-filter (lambda (o) (plist-get o :speakers))
(emacsconf-active-talks (emacsconf-filter-talks info)))))
(by-status (seq-group-by (lambda (o) (plist-get o :status)) talks))
(files (directory-files emacsconf-backstage-dir)))
(insert
"<html><head><meta charset=\"UTF-8\"><link rel=\"stylesheet\" href=\"/style.css\" /></head><body>"
(if (file-exists-p (expand-file-name "include-in-index.html" emacsconf-cache-dir))
(with-temp-buffer (insert-file-contents (expand-file-name "include-in-index.html" emacsconf-cache-dir)) (buffer-string))
"")
"<p>Schedule by status: (gray: waiting, light yellow: processing, yellow: to assign, light green: captioning, green: captioned and ready)<br />Updated by conf.org and the wiki repository</br />"
(let* ((emacsconf-schedule-svg-modify-functions '(emacsconf-schedule-svg-color-by-status))
(img (emacsconf-schedule-svg 800 200 info)))
(with-temp-buffer
(mapc (lambda (node)
(dom-set-attribute
node 'href
(concat "#" (dom-attr node 'data-slug))))
(dom-by-tag img 'a))
(svg-print img)
(buffer-string)))
"</p>"
(if (eq emacsconf-backstage-phase 'prerec)
(format "<p>Waiting for %d talks (~%d minutes) out of %d total</p>"
(length (assoc-default "WAITING_FOR_PREREC" by-status))
(emacsconf-sum :time (assoc-default "WAITING_FOR_PREREC" by-status))
(length talks))
"")
"<ul>"
(mapconcat
(lambda (status)
(concat "<li>"
(if (string= status "TO_ASSIGN")
"TO_ASSIGN (waiting for volunteers)"
status)
": "
(mapconcat (lambda (o) (format "<a href=\"#%s\">%s</a>"
(plist-get o :slug)
(plist-get o :slug)))
(assoc-default status by-status)
", ")
"</li>"))
(pcase emacsconf-backstage-phase
('prerec'("TO_PROCESS""PROCESSING""TO_ASSIGN""TO_CAPTION""TO_STREAM"))
('harvest'("TO_ARCHIVE""TO_REVIEW_QA""TO_INDEX_QA""TO_CAPTION_QA")))
"")
"</ul>"
(pcase emacsconf-backstage-phase
('prerec
(concat
(emacsconf-publish-backstage-processing by-status files)
(emacsconf-publish-backstage-to-assign by-status files)
(emacsconf-publish-backstage-to-caption by-status files)
(emacsconf-publish-backstage-to-stream by-status files)))
('harvest
(let ((stages
'(("TO_REVIEW_QA" .
"Please review the --bbb-webcams.webm file and/or the --bbb-webcams.vtt and tell us (emacsconf-submit@gnu.org) if a Q&A session can be published or if it needs to be trimmed (lots of silence at the end of the recording, accidentally included sensitive information, etc.).")
("TO_INDEX_QA" .
"Please review the --answers.webm and --answers.vtt files to make chapter markers so that people can jump to specific parts of the Q&A session. The <a href=\"https://emacsconf.org/harvesting/\">harvesting page on the wiki</a> has some notes on the process. That way, it's easier for people to see the questions andanswers without needing to listen to everything again. You can see <a href=\"https://emacsconf.org/2022/talks/asmblox\">asmblox</a> for an example of the Q&A chapter markers.")
("TO_CAPTION_QA" .
"Please edit the --answers.vtt for the Q&A talk you're interested in, correcting misrecognized words and cleaning it up so that it's nice to use as closed captions. All talks should now have large-model VTTs to make it easier to edit."))))
(mapconcat
(lambda (stage)
(let ((status (car stage)))
(format
"<h1>%s: %d talk(s) (%d minutes)</h1>%s<ol class=\"videos\">%s</ol>"
status
(length (assoc-default status by-status))
(emacsconf-sum :video-time (assoc-default status by-status))
(cdr stage)
(mapconcat
(lambda (f)
(format "<li><a name=\"%s\"></a><strong><a href=\"%s%s\">%s</a></strong><br />%s (id:%s)<br />%s</li>"
(plist-get f :slug)
emacsconf-base-url
(plist-get f :url)
(plist-get f :title)
(plist-get f :speakers-with-pronouns)
(plist-get f :slug)
(emacsconf-publish-index-card
(append (list
:video-note
(unless (file-exists-p (expand-file-name (concat (plist-get f :file-prefix) "--bbb-webcams.webm") emacsconf-cache-dir))
"<div>No Q&A video for this talk</div>")
:video-file
(cond
((file-exists-p (expand-file-name (concat (plist-get f :file-prefix) "--answers.webm") emacsconf-cache-dir))
(concat (plist-get f :file-prefix) "--answers.webm"))
((file-exists-p (expand-file-name (concat (plist-get f :file-prefix) "--bbb-webcams.webm") emacsconf-cache-dir))
(concat (plist-get f :file-prefix) "--bbb-webcams.webm"))
(t t)) ;; omit video:video-id"-qanda":extra
(concat
(emacsconf-surround "QA note: " (plist-get f :qa-note) "<br />")
(format "Q&A archiving: <a href=\"%s-%s.txt\">IRC: %s-%s</a>"
(format-time-string "%Y-%m-%d" (plist-get f :start-time))
(plist-get (emacsconf-get-track f) :channel)
(format-time-string "%Y-%m-%d" (plist-get f :start-time))
(plist-get (emacsconf-get-track f) :channel))
(emacsconf-surround ", <a href=\""
(if (file-exists-p (expand-file-name (concat (plist-get f :file-prefix) "--pad.txt")
emacsconf-cache-dir))
(concat (plist-get f :file-prefix) "--pad.txt"))
"\">Etherpad (Markdown)</a>""")
(emacsconf-surround ", <a href=\"" (plist-get f :bbb-playback) "\">BBB playback</a>""")
(emacsconf-surround ", <a href=\""
(if (file-exists-p (expand-file-name (concat (plist-get f :file-prefix) "--bbb.txt")
emacsconf-cache-dir))
(concat (plist-get f :file-prefix) "--bbb.txt"))
"\">BBB text chat</a>""")
(emacsconf-surround ", <a href=\""
(if (file-exists-p (expand-file-name (concat (plist-get f :file-prefix) "--bbb-webcams.opus")
emacsconf-cache-dir))
(concat (plist-get f :file-prefix) "--bbb-webcams.opus"))
"\">BBB audio only</a>"""))
:files (emacsconf-publish-talk-files f files))
f)
emacsconf-main-extensions)))
(assoc-default status by-status) "\n"))))
stages
"\n"))))
(if (file-exists-p (expand-file-name "include-in-index-footer.html" emacsconf-cache-dir))
(with-temp-buffer (insert-file-contents (expand-file-name "include-in-index-footer.html" emacsconf-cache-dir)) (buffer-string))
"")
"</body></html>")))))
For example, the section for talks that are waiting for volunteers is handled by the function emacsconf-publish-backstage-to-assign.
emacsconf-publish-backstage-to-assign
(defunemacsconf-publish-backstage-to-assign (by-status files)
(let ((list (assoc-default "TO_ASSIGN" by-status)))
(format "<h1>%s talk(s) to be captioned, waiting for volunteers (%d minutes)</h1><p>You can e-mail <a href=\"mailto:sacha@sachachua.com\">sacha@sachachua.com</a> to call dibs on editing the captions for one of these talks. We use OpenAI Whisper to provide auto-generated VTT that you can use as a starting point, but you can also write the captions from scratch if you like. If you're writing them from scratch, you can choose to include timing information, or we can probably figure them out afterwards with a forced alignment tool. More info: <a href=\"https://emacsconf.org/captioning/\">captioning tips</a></p><ul class=\"videos\">%s</ul>"
(length list)
(emacsconf-sum :video-time list)
(mapconcat
(lambda (f)
(setq f (append
f
(list :extra
(if (plist-get f :caption-note) (concat "<div class=\"caption-note\">" (plist-get f :caption-note) "</div>") "")
:files
(emacsconf-publish-talk-files f files))))
(format "<li><a name=\"%s\"></a><strong><a href=\"%s\">%s</a></strong><br />%s (id:%s)<br />%s</li>"
(plist-get f :slug)
(plist-get f :absolute-url)
(plist-get f :title)
(plist-get f :speakers)
(plist-get f :slug)
(emacsconf-publish-index-card f)))
list
"\n"))))
Each talk has a little card that includes its video and links to files.
emacsconf-publish-index-card: Format an HTML card for TALK, linking the files in EXTENSIONS.
(defunemacsconf-publish-index-card (talk &optional extensions)
"Format an HTML card for TALK, linking the files in EXTENSIONS."
(let* ((file-prefix (plist-get talk :file-prefix))
(video-file (plist-get talk :video-file))
(video (and file-prefix
(emacsconf-publish-index-card-video
(or (plist-get talk :video-id)
(concat (plist-get talk :slug) "-mainVideo"))
video-file talk extensions))))
;; Add extra information to the talk
(setq talk
(append
talk
(list
:time-info (emacsconf-surround "Duration: " (plist-get talk :video-duration) " minutes""")
:video-html (or (plist-get video :video) "")
:audio-html (or (plist-get video :audio) "")
:chapter-list (or (plist-get video :chapter-list) "")
:resources (or (plist-get video :resources) "")
:extra (or (plist-get talk :extra) "")
:speaker-info (or (plist-get talk :speakers-with-pronouns) ""))))
(emacsconf-replace-plist-in-string
talk
"<div class=\"vid\">${video-html}${audio-html}<div>${extra}</div>${time-info}${resources}${chapter-list}</div>")))
So it's all built on templates and a little bit of code to make the
appropriate lists. You can find this code in
emacsconf-publish.el.
announce it on IRC so that the talk details split up the chat log and make it easier to read
publish the media files to https://media.emacsconf.org/2023 (currently password-protected while testing) so that people can view them
update the talk wiki page with the media files and the captions
update the Youtube and Toobnix videos so that they're public: this
is manual for the moment, since I haven't put time into automating
it yet
I have code for most of this, and we've done it successfully for the
past couple of years. I just need to refamiliarize myself with how to
do it and how to set it up for testing during the dry run, and modify
it to work with the new crontab-based system.
Last year, I set up the server with the ability to act as the
controller, so that it wasn't limited to my laptop. The organizers
notebook says I put it in the orga@ user's account, and I probably
ran it under a screen session. I've submitted a talk for this year's
conference, so I can use a test video for that one. First, I need to
update the Ansible configuration for publishing and editing:
I needed to add a version attribute to the git repo checkout in the
Ansible playbook, since we'd switched from master to main. I also
needed to set emacs_version to 29.1 since I started using
seq-keep in my Emacs Lisp functions. For testing, I set
emacsconf-publishing-phase to conference.
Act on TODO state changes
org-after-todo-state-change-hook makes it easy to automatically run
functions when the TODO state changes. I add this hook that runs a
list of functions and passes the talk information so that I don't have
to parse the talk info in each function.
emacsconf-org-after-todo-state-change: Run all the hooks in ‘emacsconf-todo-hooks’.
(defunemacsconf-org-after-todo-state-change ()
"Run all the hooks in `emacsconf-todo-hooks'.If an `emacsconf-todo-hooks' entry is a list, run it only for thetracks with the ID in the cdr of that list."
(let* ((talk (emacsconf-get-talk-info-for-subtree))
(track (emacsconf-get-track (plist-get talk :track))))
(mapc
(lambda (hook-entry)
(cond
((symbolp hook-entry) (funcall hook-entry talk))
((member (plist-get track :id) (cdr hook-entry))
(funcall (car hook-entry) talk))))
emacsconf-todo-hooks)))
This can be enabled and disabled with the following functions.
emacsconf-add-org-after-todo-state-change-hook: Add FUNC to ‘org-after-todo-stage-change-hook’.
We used to scramble to upload all the videos in the days or weeks
after the conference, since the presentations were live. Since we
switched to encouraging speakers to upload videos before the
conference, we've been able to release the videos pretty much as soon
as the talk starts playing. This code in
emacsconf-publish.el takes care of copying the files from
the backstage to the public media directory, republishing the index,
and republishing the playlist. That way, people who come in late or
who want to refer to the video can easily get the full video right
away.
emacsconf-publish-media-files-on-change: Publish the files and update the index.
This is low-priority, but it might be nice to figure out. The easiest
way is probably to use open the Youtube/Toobnix URLs on my computer
and then use either Tampermonkey or Spookfox to set the talk to
public. Someday!
Update the talk status on the server
Last year, I experimented with having the shell scripts automatically
update the status of the talk from TO_STREAM to PLAYING and from
PLAYING to CLOSED_Q. Since I've moved the talk-running into
track-specific crontabs, now I need to sudo back to the orga user
and set XDG_RUNTIME_DIR in order to use emacsclient. I can call
this with sudo -u orga talk $slug $status in the
roles/obs/templates/handle-session script.
#!/bin/bash# # How to use: talk slug from-status-regexp to-status# or talk slug to-statusSLUG="$1"FROM_STATUS="$2"TO_STATUS="$3"XDG_RUNTIME_DIR=/run/user/
if [ "x$TO_STATUS" == "x" ]; thenFROM_STATUS=.
TO_STATUS="$2"ficd#echo "Pulling conf.org..."#git pullecho"Updating status..."XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR emacsclient --eval "(emacsconf-with-todo-hooks (emacsconf-update-talk-status \"$SLUG\" \"$FROM_STATUS\" \"$TO_STATUS\"))" -a emacs
#echo "Committing and pushing in the background"#git commit -m "Update task status for $SLUG from $FROM_STATUS to $TO_STATUS" conf.org#git push &
Testing notes
Looks like everything works fine when I run it from the crontab: the
talk status is updated, the media files are published, the wiki is
updated, and the talks are announced on IRC. Backup plan A is to
manually control the talk status using Emacs on the server. Backup
plan B is to control the talk status using Emacs on my laptop. Backup
plan C is to call the individual functions instead of relying on the
todo state change functions. I think it'll all work out, although I'll
probably want to do another dry run at some point to make sure. Slowly
getting there…
[2023-10-26 Thu]: updated handle-session and added talk
I figured out multi-track streaming so close to EmacsConf 2022 that
there wasn't enough time to get other volunteers used to working with
the setup, especially since I was still scrambling to figure out more
infrastructure as the conference approached. We decided I'd run both
streams myself, which meant I needed to make things as automatic as
possible so that I wouldn't go crazy. I wanted a lot of things to
happen automatically: playing recorded intros and videos, browsing to
the right URLs depending on the type of Q&A, publishing updates to the
wiki, and so on.
I used timers and TODO state changes to execute commands via TRAMP,
which was pretty cool for the most part. But it turned out TRAMP
doesn't like being called when it's already running, like when it's
being called from two timers going off at the same time. It gives a
"Forbidden reentrant call of TRAMP". We found a couple of quick
workarounds: I could reschedule the talks to be a minute apart, or I
could cancel the conflicting timer and just start them with the shell
scripts.
Last year, we had a shell script that played the intro and the main
talk, and other scripts to handle the Q&A by opening BigBlueButton,
Etherpad, or the IRC channel. Much of the logic was in Emacs Lisp
because it was easy to write it that way. For this year, I wanted to
write a script that handled the intro, video, and Q&A portions. This
is now in roles/obs/templates/handle-session.
handle-session
#!/bin/bash# ## Handle the intro/talk/Q&A for a session# Usage: handle-session $SLUGYEAR=""BASE_DIR=""FIREFOX_NAME=firefox-esr
SLUG=$1# Kill background music if playingif screen -list | grep -q background; then
screen -S background -X quit
fi# Update the status
sudo -u talk $SLUG PLAYING &
# Update the overlay
overlay $SLUG# Play the intro if it exists. If it doesn't exist, switch to the intro slide and stop processing.if [[ -f $BASE_DIR/assets/intros/$SLUG.webm ]]; then
killall -s TERM $FIREFOX_NAME
mpv $BASE_DIR/assets/intros/$SLUG.webm
else
firefox --kiosk $BASE_DIR/assets/in-between/$SLUG.png
exit 0
fi# Play the video if it exists. If it doesn't exist, switch to the BBB room and stop processing.if [ "x$TEST_MODE" = "x" ]; thenLIST=($BASE_DIR/assets/stream/--$SLUG*--main.webm)
elseLIST=($BASE_DIR/assets/test/--$SLUG*--main.webm)
fiFILE="${LIST[0]}"if [ ! -f "$FILE" ]; then# Is there an original file?LIST=($BASE_DIR/assets/stream/--$SLUG*--original.{webm,mp4,mov})
FILE="${LIST[0]}"fiif [[ -f $FILE ]]; then
killall -s TERM $FIREFOX_NAME
mpv $FILEelse
/usr/local/bin/bbb $SLUGexit 0
fi
sudo -u talk $SLUG CLOSED_Q &
# Open the appropriate Q&A URLQA=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["qa-backstage-url"]' < $BASE_DIR/talks.json)
QA_TYPE=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["qa-type"]' < $BASE_DIR/talks.json)
echo"QA_TYPE $QA_TYPE QA $QA"if [ "$QA_TYPE" = "live" ]; then
/usr/local/bin/bbb $SLUGelif [ "$QA" != "null" ]; then
/usr/local/bin/music &
/usr/bin/firefox $QA# i3-msg 'layout splith'fiwait
Now that we have a script that handles all the different things
related to a session, it's easier to schedule the execution of that
script. Instead of using Emacs timers and running into that problem
with tramp, I want to try using cron. Cron is a standard UNIX and
Linux tool for scheduling things to run at certain times. You make a
plain text file in a particular format: minute, hour, day of month,
month, day of week, and then the command, and then you tell cron to
use that file with something like crontab your-file. Since it's
plain text, we can generate it with Emacs Lisp and
format-time-string, save with TRAMP, and install with ssh. Each
track has its own user account for streaming, so each track can have
its own file.
emacsconf-stream-format-crontab: Return crontab entries for TALKS.
(defunemacsconf-stream-format-crontab (track talks &optional test-mode)
"Return crontab entries for TALKS.Use the display specified in TRACK.If TEST-MODE is non-nil, load the videos from the test directory."
(concat
(format
"PATH=/usr/local/bin:/usr/binMAILTO=\"\"XDG_RUNTIME_DIR=\"/run/user/%d\"" (plist-get track :uid))
(mapconcat
(lambda (talk)
(format "%s /usr/bin/screen -dmS play-%s bash -c \"DISPLAY=%s TEST_MODE=%s /usr/local/bin/handle-session %s | tee -a ~/track.log\"\n";; cron times are UTC
(format-time-string "%-M %-H %-d %m *" (plist-get talk :start-time))
(plist-get talk :slug)
(plist-get track :vnc-display)
(if test-mode "1""")
(plist-get talk :slug)))
(emacsconf-filter-talks talks))))
emacsconf-stream-crontabs: Write the streaming users’ crontab files.
(defunemacsconf-stream-crontabs (&optional test-mode info)
"Write the streaming users' crontab files.If TEST-MODE is non-nil, use the videos in the test directory.If INFO is non-nil, use that as the schedule instead."
(interactive)
(let ((emacsconf-publishing-phase 'conference))
(setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
(dolist (track emacsconf-tracks)
(let ((talks (seq-filter (lambda (talk)
(string= (plist-get talk :track)
(plist-get track :name)))
info))
(crontab (expand-file-name (concat (plist-get track :id) ".crontab")
(concat (plist-get track :tramp) "~"))))
(with-temp-file crontab
(when (plist-get track :autopilot)
(insert (emacsconf-stream-format-crontab track talks test-mode))))
(emacsconf-stream-track-ssh track (concat "crontab ~/" (plist-get track :id) ".crontab"))))))
I want to test the whole setup before the conference, of course.
First, I needed test videos. This generates test videos and subtitles
following our naming convention.
Then I needed to write a crontab based on a different schedule. This
code sets up a series of test videos to start about a minute after I
run the code, with the dev stream set up to start a minute after the
gen stream.
I needed to use timedatectl set-timezone America/Toronto to change
the server's timezone to America/Toronto so that the crontab would
run at the right time.
I also needed to specify the PATH so that I didn't need to add the
absolute paths in all the other shell scripts, XDG_RUNTIME_DIR to
get audio working, and DISPLAY so that windows showed up in the
right place.
I think this will let me run both tracks for EmacsConf with more ease
and less frantic juggling. We'll see!
During EmacsConf, we join BigBlueButton webconferences for live
presentations and Q&A sessions so that the speakers and the host can
be on stream. I wanted to reduce the number of manual steps needed to
join the web conference, since any clicks or keystrokes would need to
be done via a VNC connection. I used Tampermonkey to write a script to
join BigBlueButton and set things up the way we want to.
I don't have the Tampermonkey installation and setup automated with
Ansible yet, but here's what I did for each track:
// ==UserScript==// @name Connect to EmacsConf chat automatically// @namespace https://emacsconf.org/// @version 0.1// @description try to take over the world!// @author You// @match https://chat.emacsconf.org/*// @icon https://www.google.com/s2/favicons?sz=64&domain=emacsconf.org// @grant none// ==/UserScript==
(function() {
'use strict';
setTimeout(() => {
if (document.querySelector('.connect-row')) {
document.querySelector('.connect-row').closest('form').querySelector('button').click();
}
}, 1000);
})();
Join an BBB meeting. Check the address bar to see if autoplay is
disabled (crossed-out autoplay icon). If it is, click on it and
change Block audio to Allow audio and video.
Now I can use this Ansible template for a shell script to connect to the BBB session (roles/obs/templates/bbb):
#!/bin/bash# Open the Big Blue Button room using the backstage link# # Kill the background music if playingif screen -list | grep -q background; then
screen -S background -X quit
fi# Update the overlaySLUG=$1
overlay $SLUG
killall -s TERM firefox-esr
firefox https://media.emacsconf.org//backstage/assets/redirects/open/bbb-$SLUG.html &
sleep 5
xdotool search --class firefox windowactivate --sync
xdotool key Return
xdotool key F11
wait
I wanted to use Mermaid to make diagrams, but I ran into this issue when trying to run it:
Error: Could not find Chromium (rev. 1108766). This can occur if either
1. you did not perform an installation before running the script (e.g. `npm install`) or
2. your cache path is incorrectly configured (which is: /home/sacha/.cache/puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.
It turns out that I needed to do the following:
sudo npm install -g puppeteer mermaid @mermaid-js/mermaid-cli --unsafe-perm
# Cache Chromium for my own user
node /usr/lib/node_modules/puppeteer/install.js --unsafe-perm
sudo npm install -g mermaid @mermaid-js/mermaid-cli
ln -s ~/.cache/puppeteer/chrome/linux-117.0.5938.149 ~/.cache/puppeteer/chrome/linux-1108766
ln -s ~/.cache/puppeteer/chrome/linux-117.0.5938.149/chrome-linux64 ~/.cache/puppeteer/chrome/linux-117.0.5938.149/chrome-linux
(The exact versions might be different for your installation.)
Then I could make a Mermaid file and try it out with mmdc -i input.mmd -o output.svg,
and then I could confirm that it works directly from Org with ob-mermaid:
Sometimes I want to work with all the talks associated with an email
in my inbox. For example, maybe a speaker said that the draft
schedules are fine, and I want to make a note of that in the
conference Org file.
First we start with a function that gets the e-mail addresses for a
talk. Some speakers have different e-mail addresses for public contact
or private contact, and some e-mail us from other addresses.
emacsconf-mail-get-all-email-addresses: Return all the possible e-mail addresses for TALK.
(defunemacsconf-mail-get-all-email-addresses (talk)
"Return all the possible e-mail addresses for TALK."
(split-string
(downcase
(string-join
(seq-uniq
(seq-keep
(lambda (field) (plist-get talk field))
'(:email:public-email:email-alias)))
","))
" *, *"))
Then we can use that to find the talks for a given e-mail address.
emacsconf-mail-talks: Return a list of talks matching EMAIL.
(defunemacsconf-mail-talks (email)
"Return a list of talks matching EMAIL."
(setq email (downcase (mail-strip-quoted-names email)))
(seq-filter
(lambda (o) (member email (emacsconf-mail-get-all-email-addresses o)))
(emacsconf-get-talk-info)))
We can loop over that to add a note for the e-mail.
emacsconf-mail-add-to-logbook: Add to logbook for all matching talks from this speaker.
(defunemacsconf-mail-add-to-logbook (email note)
"Add to logbook for all matching talks from this speaker."
(interactive
(let* ((email (mail-strip-quoted-names
(plist-get (plist-get (notmuch-show-get-message-properties) :headers)
:From)))
(talks (emacsconf-mail-talks email)))
(list
email
(read-string (format "Note for %s: "
(mapconcat (lambda (o) (plist-get o :slug))
talks", "))))))
(save-window-excursion
(mapc
(lambda (talk)
(emacsconf-add-to-talk-logbook talk note))
(emacsconf-mail-talks email))))
The actual addition of notes is handled by these functions.
emacsconf-add-to-logbook: Add NOTE as a logbook entry for the current subtree.
(defunemacsconf-add-to-logbook (note)
"Add NOTE as a logbook entry for the current subtree."
(move-marker org-log-note-return-to (point))
(move-marker org-log-note-marker (point))
(with-temp-buffer
(insert note)
(let ((org-log-note-purpose 'note))
(org-store-log-note))))
Then we have a function that looks for the heading for a note and then
adds a logbook entry to it.
emacsconf-add-to-talk-logbook: Add NOTE as a logbook entry for TALK.
(defunemacsconf-add-to-talk-logbook (talk note)
"Add NOTE as a logbook entry for TALK."
(interactive (list (emacsconf-complete-talk) (read-string "Note: ")))
(save-excursion
(emacsconf-with-talk-heading talk
(emacsconf-add-to-logbook note))))
All together, that makes it easy to use Emacs as a very simple contact
relationship management system where I can take notes based on the
e-mails that come in.