# [[elisp:oer-reveal-export-to-html][Export as presentation HTML]] # [[elisp:(let ((res (org-export-as 'html))) (with-temp-file "exported.html" (insert res)))][Export as long document HTML]] #+SPDX-FileCopyrightText: 2023 Sacha Chua #+SPDX-License-Identifier: CC-BY-SA-4.0 #+OPTIONS: toc:nil tags:nil num:t reveal_show_notes:t #+REVEAL_THEME: simple #+REVEAL_HLEVEL: 10 #+REVEAL_INIT_OPTIONS: transition: 'none', height: 720, width: 940, center: false #+REVEAL_INIT_SCRIPT: autoPlayMedia: true #+REVEAL_HISTORY: t #+REVEAL_TRANS: none #+REVEAL_TITLE_SLIDE:

%t

%s

%A %a

emacsconf.org/2023/talks/emacsconf

Starting from the next slide, you can press “play” once in the audio controls (or type “a”) to start the presentation, which advances automatically afterwards.

# of audio: #+OER_REVEAL_AUDIO_SLIDESHOW_CONFIG: audio: { advance: 0, suffix: '.opus', autoplay: false, defaultDuration: 1, defaultAudios: false, defaultAudioRate: window.location.search.match( /audio-speed/gi )? parseFloat((new URL(window.location.href)).searchParams.get('audio-speed')) : 1.0, playerOpacity: 0.8, playerStyle: 'position: fixed; bottom: 9.5vh; left: 0%; width: 30%; height:30px; z-index: 33;', prefix: 'audio/presentation' } #+HTML_HEAD: #+Title: EmacsConf.org #+Author: Sacha Chua # COMMENT +REVEAL_ROOT: https://cdn.jsdelivr.net/npm/reveal.js@4.6.1/ # COMMENT +REVEAL_ROOT: file:///home/sacha/vendor/reveal.js # COMMENT REVEAL_ROOT: http://localhost:8000/ #+REVEAL_ROOT: ./reveal-js * Backstage :noexport: You probably won't be able to republish this file on your own because it refers to files on my computer, but I'm sharing it just in case the general ideas are handy. ** Serving the files Because I refer to media files in this presentation, it can't be loaded from just a =file://= URL. I needed to use a small web server to serve it. [[https://www.npmjs.com/package/live-server][live-server]] is a NodeJS server that was quick to install and run (assuming you have NodeJS): #+begin_src sh :eval no npm install -g live-server live-server --host=127.0.0.1 #+end_src ** Libraries I decided to experiment with using RevealJS for my presentation because I wanted people to be able to click on links, copy text, and so on. I was curious about this [[https://oer.gitlab.io/emacs-reveal-howto/tts-howto.html][oer-reveal + text-to-speech]] as a way to have a fallback in case I didn't have time to record some audio narration, but I wasn't happy with any of the voices and I was able to figure out an audio workflow for recording things, so I didn't need it. After a bit of hacking around, I managed to get [[package:org-re-reveal]] and [[package:oer-reveal]] to work. I patched reveal.js to [[https://github.com/hakimel/reveal.js/pull/3491][handle speaker notes for fragments]]. It hasn't been merged yet, so I'll refer to my copy in reveal.js. I dynamically include source code from [[https://git.emacsconf.org/emacsconf-el][emacsconf-el]] and [[https://git.emacsconf.org/emacsconf-ansible][emacsconf-ansible]]. Some code in the presentation assumes the file locations, though. Someday I'll figure out how to make that more dynamic! Extra libraries that I refer to in my presentation (including defuns, etc.) that aren't autoloaded: #+begin_src emacs-lisp (require 'emacsconf-mail) (require 'emacsconf-spookfox) #+end_src #+RESULTS: :results: emacsconf-spookfox :end: ** Emacs configuration #+begin_src emacs-lisp (use-package org-re-reveal :config (setq org-re-reveal-revealjs-version "4") (setq org-re-reveal-history t)) (use-package oer-reveal :config (setq oer-reveal-plugin-4-config "audioslideshow RevealAudioSlideshow plugin/audio-slideshow/plugin.js anything RevealAnything https://cdn.jsdelivr.net/npm/reveal.js-plugins@latest/anything/plugin.js")) #+end_src ** Licenses *** org-re-reveal, oer-reveal: GPL-3.0-or-later *** RevealJS: MIT License [[https://github.com/hakimel/reveal.js][RevealJS]] Copyright (C) 2011-2023 Hakim El Hattab, http://hakim.se, and reveal.js contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *** audio-slideshow: MIT License [[https://github.com/rajgoel/reveal.js-plugins/tree/master/audio-slideshow][audio-slideshow]] The MIT License (MIT) Copyright (c) 2021 Asvin Goel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ** Progression #+NAME: progression | Slide | Highlight | Additional elements | |-------------------+--------------------------------+-----------------------------------------------------------------------------------| | props-map | h-email;h-properties | t-email,email-properties,t-properties | | file-prefixes | h-properties;h-filename | t-filename,properties-filename | | renaming | h-filename;h-renaming | t-renaming,filename-renaming | | shell-scripts | h-renaming;h-shell-scripts | renaming-shell-scripts,t-shell-scripts | | availability | h-properties;h-timezone | t-timezone,properties-timezone | | schedule | h-timezone;h-schedule | t-schedule,timezone-schedule | | emailing-speakers | h-timezone,h-mail-merge;h-emailing-speakers | schedule-emailing-speakers,t-emailing-speakers | | template | h-properties;h-template | t-template,properties-template | | wiki | h-template;h-wiki | t-wiki,template-wiki,schedule-wiki | | pad | h-template;h-pad | template-pad,t-pad | | mail-merge | h-template;h-mail-merge | t-mail-merge,template-mail-merge,schedule-mail-merge,emailing-speakers-mail-merge | | bbb | h-bbb | t-bbb | | checkin | h-mail-merge;h-checkin | t-checkin,bbb-checkin | | redirect | h-bbb;h-redirect | t-redirect,bbb-redirect | | shortcuts | h-email;h-shortcuts | t-shortcuts,email-shortcuts | | logbook | h-shortcuts;h-logbook | shortcuts-logbook,t-logbook | | captions | h-captions | t-captions,captions-wiki | | tramp | h-captions;h-tramp | t-tramp,captions-tramp | | crontab | h-tramp;h-crontab | tramp-crontab,bbb-crontab,t-crontab | | transitions | h-crontab;h-transitions | shell-scripts-transitions,t-transitions,shortcuts-transitions,transitions-crontab | | irc | h-transitions;h-irc | t-irc,transitions-irc | Check progression #+begin_src emacs-lisp :var progression=progression (defun my-check-progression () "Make sure all the rows have corresponding slides. Check order, too." (let ((ids (org-map-entries (lambda () (org-entry-get (point) "CUSTOM_ID")) "CUSTOM_ID={.}")) (has-map (org-map-entries (lambda () (org-entry-get-with-inheritance "CUSTOM_ID")) "map")) last current result) (dolist (p progression) (setq current (seq-position ids (car p))) (if current (progn (when (and last (< current last)) (add-to-list 'result (concat (car p) " out of order: not after " (elt ids last)))) (setq last current)) (add-to-list 'result (concat (car p) " not found")))) (dolist (p (seq-difference (mapcar 'car progression) has-map)) (add-to-list 'result (format "%s does not have map" p))) (when result (add-to-list 'result (concat "Actual order should be: " (string-join has-map ",")))) (string-join (reverse result) "\n"))) (my-check-progression) #+end_src #+RESULTS: :results: :end: ** Notes Target: (* 140 20) 2800 words #+begin_src emacs-lisp (defvar my-note-words-target 2800) (defun my-org-collect-notes () (let (results) (org-block-map (lambda () (unless (org-in-commented-heading-p) (let ((elem (org-element-at-point))) (when (string= (org-element-property :type elem) "notes") (setq results (cons (string-trim (buffer-substring-no-properties (org-element-property :contents-begin elem) (org-element-property :contents-end elem))) results))))))) (reverse results))) (defun my-org-create-notes-buffer () (interactive) (let ((notes (my-org-collect-notes))) (with-current-buffer (get-buffer-create "*Notes*") (insert (string-join notes "\n\n")) (switch-to-buffer (current-buffer))))) (defun my-count-words-in-notes () (interactive) (let ((notes (my-org-collect-notes))) (with-temp-buffer (insert (string-join notes "\n")) (let ((num (count-words-region (point-min) (point-max)))) (message "%d words (%.f%% of %d, %d to go)" num (/ (* 100.0 num) my-note-words-target) my-note-words-target (- my-note-words-target num)))))) #+end_src #+RESULTS: :results: my-count-words-in-notes :end: ** Audio #+begin_src emacs-lisp :results silent :eval no ;; Make sure everything with notes has an :audio ;; or :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview.opus" (defun my-reveal-slide-id () (replace-regexp-in-string "^-\\|-$" "" (replace-regexp-in-string "[^A-Za-z0-9]+" "-" (downcase (concat (string-join (org-get-outline-path) "-") "-" (org-entry-get (point) "ITEM")))))) (org-map-entries (lambda () (org-entry-put (point) "REVEAL_EXTRA_ATTR" (format "data-audio-src=\"audio/%s.opus\"" (my-reveal-slide-id)))) "-REVEAL_EXTRA_ATTR={.}") #+end_src Compile the VTT for use with subed-record #+begin_src emacs-lisp (defun my-reveal-notes-vtt () (interactive) (let ((ms 0) results) (org-block-map (lambda () (unless (org-in-commented-heading-p) (let ((elem (org-element-at-point))) (when (string= (org-element-property :type elem) "notes") (let ((text (string-trim (buffer-substring-no-properties (org-element-property :contents-begin elem) (org-element-property :contents-end elem)))) audio-output prev-fragment prev-heading prop) (save-excursion (setq prev-heading (org-back-to-heading))) (save-excursion (setq prev-fragment (re-search-backward "#\\+ATTR_REVEAL:.*:audio \\(.+\\)" prev-heading t))) (if prev-fragment (setq audio-output (match-string 1)) (setq prop (org-entry-get (point) "REVEAL_EXTRA_ATTR")) (when (string-match "data-audio-src=\"\\(.+?\\)\"" prop) (setq audio-output (match-string 1 prop)))) (add-to-list 'results (list nil ms (+ 1000 ms) text (format "#+OUTPUT: %s" audio-output)) t) (setq ms (+ 1000 ms)))))))) (with-current-buffer (get-buffer-create "*notes.vtt*") (erase-buffer) (subed-vtt-mode) (subed-auto-insert) (subed-append-subtitle-list results) (display-buffer (current-buffer))))) #+end_src #+RESULTS: :results: my-reveal-notes-vtt :end: ** Store audio length #+begin_src emacs-lisp (defun my-reveal-cache-duration () (let ((proj-dir "~/proj/emacsconf-2023-emacsconf")) (org-map-entries (lambda () (let ((prop (org-entry-get (point) "REVEAL_EXTRA_ATTR"))) (when (and prop (string-match "audio/[-A-Za-z0-9]+?\\.opus" prop)) (org-entry-put (point) "AUDIO_DURATION_MS" (number-to-string (compile-media-get-file-duration-ms (expand-file-name (match-string 0 prop) proj-dir))))))) "REVEAL_EXTRA_ATTR={.}"))) #+end_src #+RESULTS: :results: my-reveal-cache-duration :end: #+begin_src emacs-lisp (defun my-reveal-cache-video-duration (&optional force) (let ((proj-dir "~/proj/emacsconf-2023-emacsconf")) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(?:video:\\([-~/A-Za-z0-9]+.webm\\)\\|\\(https:[^ ]+\\.gif\\)\\|file:\\([-A-Za-z0-9:]+.gif\\)\\)" nil t) (unless (and (null force) (save-match-data (org-entry-get (point) "VIDEO_DURATION"))) (org-entry-put (point) "VIDEO_DURATION" (number-to-string (compile-media-get-file-duration-ms (or (expand-file-name (match-string 1)) (match-string 2) (expand-file-name (match-string 3) proj-dir)))))))))) #+end_src #+RESULTS: :results: my-reveal-cache-video-duration :end: ** Squeeze videos as needed #+begin_src emacs-lisp (defun my-reveal-squeeze-videos () (let* ((proj-dir "~/proj/emacsconf-2023-emacsconf") (default-directory proj-dir) filename) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(?:video:\\([-A-Za-z0-9]+.webm\\)\\|\\(https:[^ ]+\\.gif\\)\\|file:\\([-A-Za-z0-9:]+.gif\\)\\)" nil t) (let* ((source (or (match-string 1) (match-string 2) (match-string 3))) (audio-duration (save-match-data (string-to-number (org-entry-get (point) "AUDIO_DURATION_MS")))) (video-duration (save-match-data (string-to-number (org-entry-get (point) "VIDEO_DURATION")))) (size (save-match-data (compile-media-video-dimensions source))) (compile-media-output-video-width (car size)) (compile-media-output-video-height (cdr size))) (unless (save-match-data (string-match "/video/" source)) (when (> video-duration audio-duration) (save-match-data (setq filename (expand-file-name (concat (my-reveal-slide-id) ".webm") (expand-file-name "video" proj-dir))) (compile-media-sync `((video (:source ,source :duration ,audio-duration))) filename)) (when (file-exists-p filename) (replace-match (concat "video:" filename) t))))))))) #+end_src ** Including the comments #+begin_src emacs-lisp (defvar my-subed-list nil) (defun my-subed-copy-comment () (interactive) (let* ((re (concat "^ *" (regexp-quote (replace-regexp-in-string "[^A-Za-z0-9]+" "" (subed-subtitle-text))))) (match (seq-find (lambda (o) (string-match re (replace-regexp-in-string "[^A-Za-z0-9]+" "" (elt o 3)))) my-subed-list))) (when match (subed-jump-to-subtitle-time-start) (insert (elt match 4))) (subed-forward-subtitle-text))) #+end_src Now I need to compare my scripted text with the actual text. There's the semi-accurate text from Google Translate, and I could also use Whisper. The talk recording has longer text. I can show the matching subtitles in a posframe. [[file:~/sync/Phone/Emacsconf talk recording.vtt]] [[file:~/proj/emacsconf-2023-emacsconf/emacsconf-compiled.vtt]] ** Experimenting with exporting only maps #+begin_src emacs-lisp (save-excursion (goto-char (org-babel-find-named-block "progression-css")) (org-babel-execute-src-block)) (let ((org-tags-exclude-from-inheritance "map") (org-export-select-tags '("map"))) (oer-reveal-export-to-html)) #+end_src #+RESULTS: :results: ~/proj/emacsconf-2023-emacsconf/index.html :end: ** Export and browse to current slide if custom_id is specified #+begin_src emacs-lisp (defun my-oer-reveal-export-to-html-and-browse-this-slide (&optional async subtreep visible-only body-only ext-plist) (interactive) (let ((filename (expand-file-name (oer-reveal-export-to-html async subtreep visible-only body-only ext-plist)))) (browse-url (concat (browse-url-file-url filename) (if (org-entry-get (point) "CUSTOM_ID") (concat "#slide-" (org-entry-get (point) "CUSTOM_ID")) ""))))) #+end_src #+begin_src emacs-lisp :results silent (defun my-org-re-reveal-set-footer-to-details () (interactive) (org-map-entries (lambda () (let ((details (org-entry-get (point) "DETAILS"))) (org-map-entries (lambda () (unless (org-entry-get (point) "NOSLIDE") (org-entry-put (point) "REVEAL_SLIDE_FOOTER" (format "%s" details details)))) nil 'tree))) "DETAILS={.}")) (my-org-re-reveal-set-footer-to-details) #+end_src #+RESULTS: :results: ((nil) (nil) (nil) (nil nil nil nil) (nil) (nil nil nil nil) (nil nil nil nil nil nil) (nil nil nil nil nil nil) (nil) (nil nil nil nil nil nil nil nil) (nil nil nil nil) (nil) (nil nil nil nil nil nil) (nil) (nil nil nil nil nil) (nil nil nil nil) (nil nil nil)) :end: #+begin_src emacs-lisp :results silent (save-excursion (my-org-re-reveal-set-footer-to-details)) #+end_src *** Check if all the audio files got produced #+begin_src emacs-lisp (save-excursion (goto-char (point-min)) (let (results) (while (re-search-forward "audio/[-A-Za-z0-9]+?\\.opus" nil t) (unless (or (org-in-commented-heading-p) (file-exists-p (expand-file-name (match-string 0) "~/proj/emacsconf-2023-emacsconf"))) (add-to-list 'results (match-string 0) t) )) (string-join results "\n"))) #+end_src #+RESULTS: :results: :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab.opus" :end: * Overview :PROPERTIES: :CUSTOM_ID: overview :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview.opus" :AUDIO_DURATION_MS: 15860.0 :END: How we use Org Mode and TRAMP to organize and run a multi-track conference #+begin_notes Hi, I'm Sacha Chua. This presentation is a quick tour of some of the things we do to run EmacsConf. Since 2019, we've run it as an entirely online conference, and we do as much of the organization as possible within Emacs itself. #+end_notes ** Reasons for making this presentation :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview-three-reasons.opus" :CUSTOM_ID: reasons :AUDIO_DURATION_MS: 3166.5 :END: #+begin_notes I have three reasons for making this presentation. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/overview-reason-1.opus - Revisit scrambled-together code and document things along the way #+begin_notes The first is entirely selfish: I need to figure out all the stuff I built for last year's EmacsConf, since it was a bit of a crazy scramble. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/overview-reason-2.opus - Show the process of thinking about a complex project and building it up #+begin_notes The second is that I want to show people the process of thinking about a complex project, looking for little things to automate in Emacs, and building things up from small pieces. Maybe you'll get some ideas and start building tools for yourself, too. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/overview-reason-3.opus - Point to more information \\ https://emacsconf.org/2023/talks/emacsconf #+begin_notes The third is that you find any of these little tools interesting, I want to point you to blog posts and source code where you can find out more. That way, you don't need to try to read and understand everything quickly. You can find this presentation and other links on the talk page at emacsconf.org/2023/talks/emacsconf. #+end_notes ** Map :map: :PROPERTIES: :CUSTOM_ID: map :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview-map.opus" :AUDIO_DURATION_MS: 4866.5 :END: #+ATTR_HTML: :class r-stretch [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes There are a lot of different parts, so I'll try to use this map to help make sense of it all. #+end_notes #+begin_export html #+end_export #+begin_src emacs-lisp :results silent :exports none (defun my-reveal-svg-animation (slide) (string-join (seq-map-indexed (lambda (step-ids i) (format "%s { fill: #f6f396; transition: fill %ds; transition-delay: %ds }" (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id)) (split-string step-ids ",") ", ") highlight-duration (* i highlight-duration))) (split-string (elt slide 1) ";")) "\n")) (defun my-reveal-svg-highlight-different-colors (slide) (let* ((colors '("#f6f396" "#c6c6c6")) ; reverse (steps (split-string (elt slide 1) ";")) (step-length 0.5)) (string-join (seq-map-indexed (lambda (step-ids i) (format "%s { fill: %s; transition: fill %.1fs; transition-delay: %.1fs }" (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id)) (split-string step-ids ",") ", ") (elt colors (- (length steps) i 1)) step-length (* i 0.5))) steps)))) #+end_src #+NAME: progression-css #+begin_src emacs-lisp :exports results :var map-progression=progression :var highlight-duration=2 :wrap export html (let (full) (format "" (mapconcat (lambda (slide) (setq full (append (split-string (elt slide 2) ",") full)) (format "#slide-%s.present #text path { opacity: 0.2 } #slide-%s.present #arrows path { opacity: 0.4 } %s { opacity: 1 !important } %s" (car slide) (car slide) (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id)) full ", ") (my-reveal-svg-highlight-different-colors slide) )) map-progression "\n" ))) #+end_src #+RESULTS: progression-css #+begin_export html #+end_export * Organizing information :PROPERTIES: :CUSTOM_ID: info :REVEAL_EXTRA_ATTR: data-audio-src="audio/info.opus" :AUDIO_DURATION_MS: 5246.5 :END: #+begin_notes There's so much information to work with, so it probably doesn't surprise you that we use Org Mode a lot. #+end_notes #+begin_export html #+end_export #+ATTR_REVEAL: :frag t :audio audio/info-email.opus - E-mail: Notmuch + Emacs #+begin_notes Most of the conference coordination happens over e-mail, which I can quickly search with notmuch. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-conf.opus - Private conf.org #+begin_notes Some of the information is private, like emergency contact numbers. We store the talk information in a private Org file. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-organizers-notebook.opus - [[https://emacsconf.org/2023/organizers-notebook/][Public organizers' notebook]] #+begin_notes I try to put as much as possible into our public organizers' notebook so that processes and decisions are documented. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-public.opus - Public webpages #+begin_notes We need a public website. We use Ikiwiki to make the webpages because we can work with plain text files in a Git repository. We also make a few static HTML pages for things where Ikiwiki is a little awkward. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-lists.opus - Mailing lists #+begin_notes We post announcements to mailing lists. We also receive submissions in a private mailing list so that a number of people can review them. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-backstage.opus - Backstage area #+begin_notes We have a backstage area for sharing files with volunteers and speakers. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-media.opus - Public media files #+begin_notes We share those files publicly when the talk goes live. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-config.opus - Configuration, etc. #+begin_notes And there's all the other stuff that goes into running EmacsConf, like shell scripts and configuration files. #+end_notes ** Storing talk information as properties :map: :PROPERTIES: :CUSTOM_ID: props-map :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-storing-talk-information-as-properties.opus" :AUDIO_DURATION_MS: 8966.5 :END: #+ATTR_HTML: :class r-stretch [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes First, speakers propose a talk by sending an e-mail. We take the info from that e-mail and store it in Org properties so that we can work with it later. #+end_notes ** Basic talk properties :PROPERTIES: :CUSTOM_ID: talk-props :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-basic-talk-properties.opus" :AUDIO_DURATION_MS: 13766.5 :END: #+begin_src org ,* WAITING_FOR_PREREC EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference SCHEDULED: <2023-12-03 Sun 14:55-15:15> :PROPERTIES: :CUSTOM_ID: emacsconf :SLUG: emacsconf :NAME: Sacha Chua :NAME_SHORT: Sacha :END: #+end_src #+begin_notes Every talk is identified with an ID, but since =:ID:= and =:CUSTOM_ID:= have special meanings for Org, I use =:SLUG:= as the keyword. Speakers' names go into the =:NAME:= property, and a short version goes into =:NAME_SHORT:= so that we can include that in a greeting. #+end_notes ** Capturing a talk proposal from an e-mail :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :CUSTOM_ID: email-capture :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-capturing-a-talk-proposal-from-an-e-mail.opus" :AUDIO_DURATION_MS: 2186.5 :END: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/2023-09-05_13-09-57.png #+begin_notes If people follow the template closely... #+end_notes ** Parsing a submission from e-mail :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :CUSTOM_ID: email-parse :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-parsing-a-submission-from-e-mail.opus" :AUDIO_DURATION_MS: 8046.5 :END: #+begin_r-stretch [[defun:emacsconf-mail-parse-submission?bare=1][emacsconf-mail-parse-submission: Extract data from EmacsConf 2023 submissions in BODY.]] #+end_r-stretch #+begin_notes ...we can even automatically fill in the Org subtree for their talk. We can use regular expressions to recognize the text and extract the properties. #+end_notes ** Setting the property to a region :PROPERTIES: :CUSTOM_ID: set-property :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-setting-the-property-to-a-region.opus" :AUDIO_DURATION_MS: 12686.5 :END: [[defun:my-org-set-property?bare=1][my-org-set-property: In the current entry, set PROPERTY to VALUE.]] #+begin_notes Other properties need to be set by hand. I often mess things up when I retype them. To avoid typos, I have a function that sets a property based on the current region. I bind that to =C-c C-x p=. #+end_notes ** Setting properties :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-setting-properties.opus" :AUDIO_DURATION_MS: 5366.5 :VIDEO_DURATION: 16030.000000000002 :END: [[video:video/organizing-information-setting-properties.webm?controls=1&autoplay=1]] #+begin_notes That makes it much easier to set properties that couldn't automatically be recognized. #+end_notes * Calculating and then editing properties :PROPERTIES: :CUSTOM_ID: file-prefixes :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties.opus" :AUDIO_DURATION_MS: 5906.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties.opus" :AUDIO_DURATION_MS: 5906.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Sometimes it makes sense to dynamically generate a property and then edit it, like with filenames. #+end_notes ** Calculating and then editing properties :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties-calculating-and-then-editing-properties.opus" :AUDIO_DURATION_MS: 11586.5 :END: #+begin_example :FILE_PREFIX: emacsconf-year-slug--title--speakers #+end_example #+begin_notes We like to name all the talk files the same way, but sometimes special characters in talk titles or speaker names need a little tweaking. I'll put that in a =FILE_PREFIX= property so I can edit it. #+end_notes ** Setting properties that are unset :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties-setting-properties-that-are-unset.opus" :AUDIO_DURATION_MS: 5786.5 :END: [[defun:emacsconf-set-file-prefixes?bare=1][emacsconf-set-file-prefixes: Set the FILE_PREFIX property for each talk entry that needs it.]] #+begin_notes An Org property match can map over all the talk entries that don't have =FILE_PREFIX= defined. #+end_notes * Renaming files :PROPERTIES: :CUSTOM_ID: renaming :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files.opus" :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :AUDIO_DURATION_MS: 3726.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files.opus" :AUDIO_DURATION_MS: 3726.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes We can use that =FILE_PREFIX= to rename files from Emacs. #+end_notes ** Renaming files with Emacs Lisp :PROPERTIES: :DETAILS: https://git.emacsconf.org/emacsconf-el/tree/emacsconf.el :REVEAL_SLIDE_FOOTER: https://git.emacsconf.org/emacsconf-el/tree/emacsconf.el :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-renaming-files-with-emacs-lisp.opus" :AUDIO_DURATION_MS: 5986.5 :END: #+begin_stretch [[defun:emacsconf-rename-files?bare=1]] #+end_stretch #+begin_notes With that property, we can then rename files using that prefix, some extra text, and the file extension. #+end_notes * Renaming files with shell scripts :PROPERTIES: :CUSTOM_ID: shell-scripts :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts.opus" :AUDIO_DURATION_MS: 6386.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts.opus" :AUDIO_DURATION_MS: 6386.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Sometimes it's easier to work with the data outside Emacs, like when I want to rename files with a shell script. #+end_notes ** Exporting the information as JSON :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts-exporting-the-information-as-json.opus" :AUDIO_DURATION_MS: 3366.5 :END: [[defun:emacsconf-publish-talks-json?bare=1]] #+begin_notes If I export a subset of the data as JSON or JavaScript Object Notation using =json-encode=... #+end_notes ** Using the data with JQ :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts-using-the-data-with-jq.opus" :AUDIO_DURATION_MS: 7206.5 :END: [[emacsconf-ansible:roles/prerec/templates/rename-original.sh]] #+INCLUDE: /home/sacha/proj/emacsconf/emacsconf-ansible/roles/prerec/templates/rename-original.sh src sh :eval no #+begin_notes ... then I can extract the data with =jq= and use it in shell scripts. #+end_notes * Parsing availability :PROPERTIES: :CUSTOM_ID: availability :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability.opus" :AUDIO_DURATION_MS: 11806.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability.opus" :AUDIO_DURATION_MS: 11806.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Another example of semi-structured information is speaker availability. We have speakers from all over the world, so we try to schedule live Q&A sessions when they're around. That means working with timezones. #+end_notes ** Setting the timezone :PROPERTIES: :CUSTOM_ID: set-timezone :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-setting-the-timezone.opus" :AUDIO_DURATION_MS: 5286.5 :VIDEO_DURATION: 11280.0 :END: video:video/parsing-availability-setting-the-timezone.webm?controls=1&autoplay=1 #+begin_notes Completion makes it much easier to set the timezone property without worrying about typos. #+end_notes ** The code :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-the-code.opus" :AUDIO_DURATION_MS: 6306.5 :END: [[defun:emacsconf-timezone-set?bare=1]] #+begin_notes We can take advantage of the timezone list from the [[https://github.com/md-arif-shaikh/tzc][tzc]] package, which works with Unix timezone definitions. #+end_notes ** Converting timezones and encoding the availability :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-converting-timezones-and-encoding-the-availability.opus" :AUDIO_DURATION_MS: 7026.5 :VIDEO_DURATION: 24220.0 :END: #+begin_example <= 11:00 EST #+end_example video:video/parsing-availability-converting-timezones-and-encoding-the-availability.webm?controls=1&autoplay=1 #+begin_notes Then we can convert times using Emacs. Using a standard format to encode the availability makes it easier to parse. #+end_notes ** Validating errors :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-validating-errors.opus" :AUDIO_DURATION_MS: 5026.5 :VIDEO_DURATION: 11980.0 :END: video:video/parsing-availability-validating-errors.webm?controls=1&autoplay=1 #+begin_notes I can use those availability constraints to report errors when I'm experimenting with the schedule. #+end_notes #+begin_comment TODO Update screenshot to use overlay talk? #+end_comment * Scheduling talks :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :CUSTOM_ID: schedule :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks.opus" :AUDIO_DURATION_MS: 3806.5 :END: *** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks.opus" :AUDIO_DURATION_MS: 3806.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Now that I have the availability information, I can think about scheduling. #+end_notes *** Schedule as table :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks-schedule-as-table.opus" :AUDIO_DURATION_MS: 11906.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/2023-09-10-11-41-42.svg #+begin_notes When we were planning EmacsConf 2022, the schedule was so full, I wanted to see if we could make it more manageable by splitting it up into two tracks. It was hard to think about times with just a table. #+end_notes *** Schedule as SVG :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks-schedule-as-svg.opus" :AUDIO_DURATION_MS: 7526.5 :VIDEO_DURATION: 2940.0 :END: #+begin_notes I was able to turn the schedule information into an SVG to convince the other organizers to get on board with this crazy plan. And the nice thing about SVGs is that they can even be clickable on the wiki. #+end_notes https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/opening-talk.gif *** Testing different schedules :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks-testing-different-schedules.opus" :AUDIO_DURATION_MS: 25166.5 :VIDEO_DURATION: 12530.0 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/output-2023-09-10-12:20:14.gif #+begin_notes Being able to quickly make SVGs of different schedules also helped me test scheduling ideas and think out loud. I could change the time between talks, the order of the talks, and even what tracks the talks were in. This was helpful when I needed to include some late submissions or availability changes and I wanted to ask speakers what they thought. They could see the different schedule options themselves. It's really nice to have Emacs Lisp support for working with SVGs. I also love how I can have an Emacs Lisp block in an Org Mode document that updates an SVG that I can view right there in my text editor. #+end_notes * Translating times :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :CUSTOM_ID: emailing-speakers :REVEAL_EXTRA_ATTR: data-audio-src="audio/translating-times.opus" :AUDIO_DURATION_MS: 8986.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/translating-times.opus" :AUDIO_DURATION_MS: 8986.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Setting the timezone lets me automatically translate times to the speaker's local timezone when I e-mail them. #+end_notes ** Translating times :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/translating-times-translating-times.opus" :AUDIO_DURATION_MS: 9246.5 :END: #+begin_example As of the time I write this e-mail, your tentative schedule is: Improving compiler diagnostics with Overlays Dec 2 Sat 1:00 pm EST translated to your local timezone US/Pacific: Dec 2 Sat 10:00 am PST I'm using ">= 11:00 -0500 (08:00 US/Pacific)" as the availability constraint for you when planning the talks, but since I sometimes mess up encoding these things, could you please doublecheck that this works for you? #+end_example =format-time-string= #+begin_notes That's mostly a matter of using =format-time-string= with a timezone. We use that in our templates, which I'll talk about a bit later. #+end_notes * Templates :PROPERTIES: :CUSTOM_ID: template :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates.opus" :AUDIO_DURATION_MS: 3626.5 :END: *** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates.opus" :AUDIO_DURATION_MS: 3626.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes It's not just about graphics. There's also a lot of text to work with, which means templates are super handy. #+end_notes ** A little function for templates :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-a-little-function-for-templates.opus" :AUDIO_DURATION_MS: 28646.5 :END: - tempo? - s-lex-format: =(let ((x 1)) (s-lex-format "x is: ${x}"))= - format: something %s something %s something %s ... ? - concat: hard to get overall view #+begin_notes There are a number of templating functions for Emacs Lisp, like the built-in =tempo.el= or =s-lex-format= from =s.el=. I ended up writing something that works with property lists (plists) instead, since we use plists all over the emacsconf-el library. All it does is replace =${variable}= with the value from a property list. I use this mostly because I have a hard time keeping track of which =%s= is which when I use =format=, and it's hard to get an overall view if I just use =concat=. #+end_notes ** The code :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-the-code.opus" :AUDIO_DURATION_MS: 6546.5 :END: [[defun:emacsconf-replace-plist-in-string?bare=1][emacsconf-replace-plist-in-string: Replace ${keyword} from ATTRS in STRING.]] #+begin_notes The code looks for the properties and replaces them with the values. I just find it a little easier to think about sometimes. #+end_notes #+begin_src emacs-lisp :eval no (emacsconf-replace-plist-in-string '(:test "Hello" :another-var "world") "Test 1: ${test} Test 2: ${another-var}") ;; Test 1: Hello Test 2: world #+end_src ** Getting all the talk information :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-getting-all-the-talk-information.opus" :AUDIO_DURATION_MS: 5986.5 :END: [[defun:emacsconf-get-talk-info?bare=1]] #+begin_notes Getting all the information is just a matter of going over all the talk entries using =org-map-entries=. #+end_notes ** Talk info functions :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-talk-info-functions.opus" :AUDIO_DURATION_MS: 3166.5 :END: [[defun:emacsconf-get-talk-info-for-subtree?bare=1]] [[defvar:emacsconf-talk-info-functions?bare=1][emacsconf-talk-info-functions: Functions to collect information.]] #+begin_notes This builds the talk info by running a bunch of functions. #+end_notes ** Getting info from the Org file :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-getting-info-from-the-org-file.opus" :AUDIO_DURATION_MS: 2926.5 :END: [[defun:emacsconf-get-talk-abstract-from-subtree?bare=1]] #+begin_notes Some functions get the information from the Org file. #+end_notes ** Modifying talk info :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-modifying-talk-info.opus" :AUDIO_DURATION_MS: 2726.5 :END: [[defun:emacsconf-add-talk-status?bare=1]] #+begin_notes Other functions use the info already collected. #+end_notes ** Performance :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-performance.opus" :AUDIO_DURATION_MS: 11346.5 :END: [[https://github.com/skeeto/emacs-memoize][memoize]] for performance #+begin_notes This can take a while to do again and again. It's useful to =memoize= this function when I know I'll be using it a lot, like when I export the organizers notebook. Memoize caches recent values. #+end_notes * Updating the conference wiki :PROPERTIES: :CUSTOM_ID: wiki :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki.opus" :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :AUDIO_DURATION_MS: 15986.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki.opus" :AUDIO_DURATION_MS: 15986.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes We combine this templating function with the talk information to fill in the conference wiki, since that's a matter of writing templated strings to files. The talk pages are generated once and then left alone for manual editing, while the navigation is regenerated every time we change the details. #+end_notes ** The conference wiki :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-the-conference-wiki.opus" :AUDIO_DURATION_MS: 12046.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/2023-09-26_14-09-07.png #+begin_notes Here are some examples of how we fill in the conference wiki. We put in the format of the talk, how Q&A works, and what the status is. Once the talk is live, we include the video and the links to the files, too. #+end_notes ** The code :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-the-code.opus" :AUDIO_DURATION_MS: 10966.5 :END: #+begin_src emacs-lisp ;; from emacsconf-publish-format-talk-schedule-info (emacsconf-replace-plist-in-string (append o (list :irc-info (format "Discuss on IRC: [#%s](%s) \n" (plist-get o :channel) (plist-get o :webchat-url)) ; ... more entries )) "[[!toc ]] Format: ${format} ${pad-info}${irc-info}${status-info}${schedule-info}\n ${alternate-apac-info}\n") #+end_src #+begin_notes The code is a little bit long, but the important part is that we fill in a plist with the values we calculate, and then we can use =emacsconf-replace-plist-in-string= to put that all together. #+end_notes ** Schedule :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-schedule.opus" :AUDIO_DURATION_MS: 8246.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/2023-09-26_14-00-19.png #+begin_notes The schedule is a little more complicated. I wrote an Ikiwiki directive so that the markup is more manageable, and the Emacs Lisp function uses that. #+end_notes *** The Ikiwiki directive :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-schedule-the-ikiwiki-directive.opus" :AUDIO_DURATION_MS: 4006.5 :END: The Ikiwiki markup looks like this: #+begin_example [[!template id=sched time="""20""" q-and-a="""BBB""" startutc="""2023-12-02T19:50:00+0000""" endutc="""2023-12-02T20:10:00+0000""" start="""2:50""" end="""3:10""" title="""EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference""" url="""/2023/talks/emacsconf""" speakers="""Sacha Chua""" track="""Development""" watch="""https://emacsconf.org/2023/watch/dev""" slug="""emacsconf""" note=""""""]] #+end_example #+begin_notes The Ikiwiki directive takes all the data and turns it into HTML... #+end_notes *** Emacs Lisp :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-schedule-emacs-lisp.opus" :AUDIO_DURATION_MS: 6766.5 :END: =emacsconf-publish-sched-directive= in [[emacsconf-el:emacsconf-publish.el]] #+begin_src emacs-lisp (format "[[!template id=sched%s]]" (let ((result "") (attrs ;; ... some code to make a property list based on the talk ... )) (while attrs (let ((field (pop attrs)) (val (pop attrs))) (when val (setq result (concat result " " (substring (symbol-name field) 1) "=\"\"\"" val "\"\"\""))))) result)) #+end_src #+begin_notes ...so we can use Emacs Lisp to iterate over a slightly smaller property list and put them into the format Ikiwiki expects. #+end_notes ** Navigation :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-navigation.opus" :AUDIO_DURATION_MS: 4786.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/2023-09-26_14-05-51.png #+begin_notes It's nice to be able to navigate between talks without going back to the schedule page each time. #+end_notes ** Emacs Lisp :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-emacs-lisp.opus" :AUDIO_DURATION_MS: 11226.5 :END: [[defun:emacsconf-publish-nav-pages?bare=1][emacsconf-publish-nav-pages: Generate links to the next and previous talks.]] #+begin_notes This is handled by keeping two extra copies of the list: one with the first talk popped off, and one with an extra element added to the beginning. Then we can use the heads of those lists for next/previous links. #+end_notes * Etherpad :PROPERTIES: :CUSTOM_ID: pad :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad.opus" :AUDIO_DURATION_MS: 1846.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Links to the next talks are also handy on the collaborative Etherpad documents that we use for collecting questions, answers, and notes during each talk. #+end_notes ** API :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad-api.opus" :AUDIO_DURATION_MS: 1846.5 :END: [[defun:emacsconf-pad-set-html?bare=1][emacsconf-pad-set-html: Set PAD-ID contents to the given HTML.]] #+begin_notes Etherpad has an API... #+end_notes ** Pad template :PROPERTIES: :DETAILS: emacsconf-pad-initial-content :REVEAL_SLIDE_FOOTER: emacsconf-pad-initial-content :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad-pad-template.opus" :AUDIO_DURATION_MS: 3526.5 :END: #+begin_example
All talks: ${talks}
${title}
${base-url}${url} - ${speakers} - Track: ${track}
Watch/participate: ${watch}
${bbb-info}
IRC: ${irc-nick-details} https://chat.emacsconf.org/#/connect?join=emacsconf,emacsconf-${track-id} or #emacsconf-${track-id} on libera.chat network
Guidelines for conduct: ${base-url}conduct
See end of file for license (CC Attribution-ShareAlike 4.0 + GPLv3 or later)
----------------------------------------------------------------
Notes, discussions, links, feedback:
----------------------------------------------------------------
... #+end_example #+begin_notes ...so I can start the pads off with a template before the conference. #+end_notes ** Checking the pad timestamp :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad-checking-the-pad-timestamp.opus" :AUDIO_DURATION_MS: 9126.5 :END: [[defun:emacsconf-pad-prepopulate-talk-pad?bare=1][emacsconf-pad-prepopulate-talk-pad: Fill in the pad for O.]] #+begin_notes I don't want to accidentally overwrite a pad that has been manually edited. We can save the timestamp of the last modification and then compare it before overwriting. #+end_notes * Mail merge :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-using-e-mail-templates-for-confirmations-and-acceptances/ :CUSTOM_ID: mail-merge :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-using-e-mail-templates-for-confirmations-and-acceptances/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/mail-merge.opus" :AUDIO_DURATION_MS: 2846.5 :END: *** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/mail-merge.opus" :AUDIO_DURATION_MS: 2846.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Templates are also very handy when it comes to e-mail. #+end_notes *** Mail merge :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-using-e-mail-templates-for-confirmations-and-acceptances/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/mail-merge-mail-merge.opus" :AUDIO_DURATION_MS: 10126.5 :END: [[defun:emacsconf-mail-review?bare=1]] #+begin_notes Sometimes we send e-mails one at a time, like when we let a speaker know that we've received their proposal. That's mostly a matter of plugging the talk's properties into the right places in the template. #+end_notes ** Mass e-mails :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-using-e-mail-templates-for-confirmations-and-acceptances/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/mail-merge-mass-e-mails.opus" :AUDIO_DURATION_MS: 24106.5 :END: [[defun:emacsconf-mail-upload-and-backstage-info?bare=1]] #+begin_notes Sometimes we send e-mails to lots of speakers at the same time, like when we send them instructions for uploading their files. Instead of sending one e-mail and Bcc-ing everyone, or sending people multiple e-mails because they have multiple talks, I like to draft these as individual e-mails to each speaker (or group of speakers, if more than one person is associated with a talk). That gives me an opportunity to personalize it further. #+end_notes * BigBlueButton :PROPERTIES: :CREATED: [2023-10-11 Wed 21:32] :DETAILS: :CUSTOM_ID: bbb :REVEAL_EXTRA_ATTR: data-audio-src="audio/bigbluebutton.opus" :AUDIO_DURATION_MS: 4106.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/bigbluebutton.opus" :AUDIO_DURATION_MS: 4106.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Many speakers answer questions live in BigBlueButton web conference rooms. #+end_notes ** Creating BigBlueButton rooms from Emacs Lisp with Spookfox :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/10/using-emacs-and-spookfox-to-automate-creating-bigbluebutton-rooms-in-mozilla-firefox/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/using-emacs-and-spookfox-to-automate-creating-bigbluebutton-rooms-in-mozilla-firefox/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/bigbluebutton-creating-bigbluebutton-rooms-from-emacs-lisp-with-spookfox.opus" :AUDIO_DURATION_MS: 20766.5 :END: [[defun:emacsconf-spookfox-create-bbb?bare=1]] #+begin_notes Setting up one room per group of speakers makes it easy to give the speakers the details and associate the recorded video with the talk afterwards. For EmacsConf 2023, I used Spookfox to control Mozilla Firefox from Emacs so that I could automate creating the rooms and adding the URLs to the talk properties in my Org file. #+end_notes * Check-in instructions :PROPERTIES: :CUSTOM_ID: checkin :REVEAL_EXTRA_ATTR: data-audio-src="audio/check-in-instructions.opus" :AUDIO_DURATION_MS: 5266.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/check-in-instructions.opus" :AUDIO_DURATION_MS: 5266.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Then I can use mail merge to send each speaker the check-in instructions for their specific room. #+end_notes ** Sending check-in instructions :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/check-in-instructions-sending-check-in-instructions.opus" :AUDIO_DURATION_MS: 8546.5 :END: [[defun:emacsconf-mail-checkin-instructions-to-all?bare=1]] #+begin_notes Some speakers will take questions by e-mail after the conference instead of attending live, so we send them shorter instructions just in case they want to drop by. #+end_notes * Redirect :PROPERTIES: :CUSTOM_ID: redirect :REVEAL_EXTRA_ATTR: data-audio-src="audio/redirect.opus" :AUDIO_DURATION_MS: 4666.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/redirect.opus" :AUDIO_DURATION_MS: 4666.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Live Q&A sessions start off with just the speaker and the host. After the first rush of questions, we can open it up for other people to join. #+end_notes ** Dynamic redirects :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/redirect-dynamic-redirects.opus" :AUDIO_DURATION_MS: 7926.5 :END: #+begin_r-stretch [[defun:emacsconf-publish-bbb-redirect?bare=1][emacsconf-publish-bbb-redirect: Update the publicly-available redirect for TALK.]] #+end_r-stretch #+begin_notes This is handled by changing the public page from one that just refreshes in a loop to one that redirects to the actual web conference room. #+end_notes ** Static redirects :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/redirect-static-redirects.opus" :AUDIO_DURATION_MS: 9206.5 :END: #+begin_r-stretch [[defun:emacsconf-publish-bbb-static-redirects?bare=1][emacsconf-publish-bbb-static-redirects: Create emergency redirects that can be copied over the right location.]] #+end_r-stretch #+begin_notes Just in case, we also generate static copies of those redirects so that we can copy them if needed. That way, I don't have to count on Emacs being able to publish them over TRAMP. #+end_notes * COMMENT TRAMP :PROPERTIES: :CREATED: [2023-10-14 Sat 15:35] :REVEAL_EXTRA_ATTR: data-audio-src="audio/comment-tramp.opus" :AUDIO_DURATION_MS: 0 :END: #+begin_src dot :file streaming-setup.png :cmdline "-Tdot | gvpr -cf ~/proj/graph/rankJustify.gvpr | neato -n -Tpng " digraph G { node [shape=box,fontname="Roboto",rankjustify="min"] rankdir=LR; "Controller Emacs" -> "emacsconf-gen" [penwidth=3,label=TRAMP]; "emacsconf-gen" -> "General stream"; "Controller Emacs" -> "emacsconf-dev" [penwidth=3]; "emacsconf-dev" -> "Development stream"; "Controller Emacs" -> "media.emacsconf.org" [penwidth=3]; "Controller Emacs" -> "local wiki git" -> "emacsconf.org wiki"; "Controller Emacs" -> IRC -> { "#emacsconf-gen", "#emacsconf-dev" }; { rank=same; "media.emacsconf.org"; "emacsconf.org wiki" } } #+end_src #+RESULTS: :results: [[file:~/proj/emacsconf-2023-emacsconf/streaming-setup.png]] :end: #+begin_notes We use TRAMP a lot. Let me explain how we've got our streaming set up. I don't have a very powerful laptop, so we can't actually stream Emacsconf from my computer. Fortunately, opalvaults very generously lets us use this server in a data center somewhere. It's got enough memory and processing power to stream both tracks, so all we need to do is connect to it over SSH or VNC to control what's on the stream. I've used ansible to make sure that the user accounts were using for the two tracks are set up the same way in terms of OBS scenes, access to files, and so on. Then I can use TRAMP to connect as that user and run whatever commands I need. So if I need to run commands on the general track, I use TRAMP to SSH as the =emacsconf-gen= user, and if I need to run commands on the development track, I ssh to emacsconf-dev. #+end_notes * Jumping to a talk :PROPERTIES: :CUSTOM_ID: shortcuts :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-jumping-to-and-working-with-talks-using-embark/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-jumping-to-and-working-with-talks-using-embark/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/jumping-to-a-talk.opus" :AUDIO_DURATION_MS: 3306.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/jumping-to-a-talk.opus" :AUDIO_DURATION_MS: 3306.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes During the conference, I'm often jumping from talk to talk. #+end_notes ** emacsconf-go-to-talk :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-jumping-to-and-working-with-talks-using-embark/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/jumping-to-a-talk-emacsconf-go-to-talk.opus" :AUDIO_DURATION_MS: 14386.5 :VIDEO_DURATION: 7930.0 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-jumping-to-and-working-with-talks-using-embark/output-2023-09-10-14:04:30.gif #+begin_notes Instead of going to the Org file and then searching for the talk, I've made a little Hydra with keyboard shortcuts. One of these shortcuts lets me jump to a talk with completion so that I can just type in part of the talk ID, title, or speaker name. #+end_notes ** Embark actions :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-jumping-to-and-working-with-talks-using-embark/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/jumping-to-a-talk-embark-actions.opus" :AUDIO_DURATION_MS: 10106.5 :VIDEO_DURATION: 18520.0 :END: video:video/jumping-to-a-talk-embark-actions.webm?controls=1&autoplay=1 #+begin_notes I've also defined some Embark actions so that I can act on a talk right from the completion menu. For example, I might want to jump to the wiki page or e-mail the speaker. #+end_notes ** COMMENT Reviewing the e-mail :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-reviewing-the-last-message-from-a-speaker/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-reviewing-the-last-message-from-a-speaker/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/jumping-to-a-talk-comment-reviewing-the-e-mail.opus" :AUDIO_DURATION_MS: 0 :END: https://sachachua.com/blog/2023/10/emacsconf-backstage-reviewing-the-last-message-from-a-speaker/2023-10-07-13-18-04.svg #+begin_notes It's really handy to manage conference e-mail from within Emacs. For example, I often worry when we haven't heard from a speaker in a long time. Did they get super busy? Am I having e-mail delivery issues? Do they have an over-enthusiastic spam filter? I can write some code to show me the last message by speaker. #+end_notes ** COMMENT Viewing the most recent thread :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-jumping-to-and-working-with-talks-using-embark/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/jumping-to-a-talk-comment-viewing-the-most-recent-thread.opus" :AUDIO_DURATION_MS: 0 :END: https://sachachua.com/blog/2023/10/emacsconf-backstage-reviewing-the-last-message-from-a-speaker/2023-10-07-18-21-55.svg #+begin_notes From there, another function makes it easy to actually view the thread. #+end_notes * Adding notes to the Org Mode logbook :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-adding-notes-to-org-logbook-drawers-from-e-mails/ :CUSTOM_ID: logbook :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-adding-notes-to-org-logbook-drawers-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/adding-notes-to-the-org-mode-logbook.opus" :AUDIO_DURATION_MS: 6386.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/adding-notes-to-the-org-mode-logbook.opus" :AUDIO_DURATION_MS: 6386.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes I can also add notes to a talk while looking at an email, like when a speaker lets me know that their video will be late. #+end_notes ** Adding a note :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-adding-notes-to-org-logbook-drawers-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/adding-notes-to-the-org-mode-logbook-adding-a-note.opus" :AUDIO_DURATION_MS: 6526.5 :VIDEO_DURATION: 22440.0 :END: video:video/adding-notes-to-the-org-mode-logbook-adding-a-note.webm?controls=1&autoplay=1 #+begin_notes Making it easy to add a note turns Emacs into a very basic contact relationship management system, or CRM. #+end_notes ** The code :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-adding-notes-to-org-logbook-drawers-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/adding-notes-to-the-org-mode-logbook-the-code.opus" :AUDIO_DURATION_MS: 13486.5 :END: [[defun:emacsconf-mail-add-to-logbook?bare=1]] #+begin_notes The way this works is that we have a function that lists all the email addresses associated with a talk. We can then map that over the list of talks, look up the author of the current email, prompt the user for the talk to add the note to, and add the note. #+end_notes ** COMMENT Confirming uploads :PROPERTIES: :CREATED: [2023-10-09 Mon 09:22] :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-adding-notes-to-org-logbook-drawers-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/adding-notes-to-the-org-mode-logbook-comment-confirming-uploads.opus" :AUDIO_DURATION_MS: 0 :END: Being able to use Emacs Lisp with emails also helps with automatically putting in information. For example, when speakers send us their recorded videos and other talk resources, I like to send an email back with the list of the files we received, their sizes, their md5sums so people can check for file corruption, and the duration of the recording. Once the files have all been renamed to match our naming convention, I can use directory-files to list all the matching files for the talk. Then I just have to go over it and get the data for the email. * Captions :PROPERTIES: :CUSTOM_ID: captions :REVEAL_EXTRA_ATTR: data-audio-src="audio/captions.opus" :AUDIO_DURATION_MS: 28246.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/captions.opus" :AUDIO_DURATION_MS: 28246.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes On to captions. We've been doing captions for the last couple of years, and now we have a small army of volunteer captioners. They get early access to the recorded talks and fix up misrecognized words, format keyboard shortcuts to follow Emacs conventions, spell names correctly, and do all sorts of other wonderful things. One of our evil plans with EmacsConf is to get cool stuff out of people's heads into videos and also make captions so that those videos can be searched. #+end_notes * Backstage index with TRAMP :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-coordinating-captioning-volunteers-using-a-backstage-area/ :CUSTOM_ID: tramp :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-coordinating-captioning-volunteers-using-a-backstage-area/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/backstage-index-with-tramp.opus" :AUDIO_DURATION_MS: 4586.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/backstage-index-with-tramp.opus" :AUDIO_DURATION_MS: 4586.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] ** The backstage index :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-coordinating-captioning-volunteers-using-a-backstage-area/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/backstage-index-with-tramp-the-backstage-index.opus" :AUDIO_DURATION_MS: 20966.5 :END: https://sachachua.com/blog/2023/10/emacsconf-backstage-coordinating-captioning-volunteers-using-a-backstage-area/2023-10-28_21-32-21.png #+begin_notes To make that possible, we first need a backstage area where volunteers can get the files. This is just a simple password-protected directory with a static HTML page that lists the talks by status and shows the files related to each talk. As a talk moves through the process, I update its TODO state and republish this index. Talks that are ready to be captioned show up in that section, and volunteers can call dibs on the talk they're interested in. #+end_notes ** Saving the file over TRAMP :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-coordinating-captioning-volunteers-using-a-backstage-area/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/backstage-index-with-tramp-saving-the-file-over-tramp.opus" :AUDIO_DURATION_MS: 5866.5 :END: #+begin_src emacs-lisp :eval no (setq emacsconf-backstage-dir "/ssh:orga@media.emacsconf.org:/var/www/media.emacsconf.org/2023/backstage") #+end_src #+begin_src emacs-lisp :eval-no (setq filename (or filename (expand-file-name "index.html" emacsconf-backstage-dir))) ;; ... (with-temp-file filename ;; ... ) #+end_src #+begin_notes That's all done with a function that formats the information and uses TRAMP to save the file directly to the server. #+end_notes ** The captioning process :PROPERTIES: :CUSTOM_ID: captioning-process :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-coordinating-captioning-volunteers-using-a-backstage-area/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/backstage-index-with-tramp-the-captioning-process.opus" :AUDIO_DURATION_MS: 8446.5 :VIDEO_DURATION: 0 :END: https://emacsconf.org/captioning #+CAPTION: Screencast of splitting subtitles [[video:video/splitting-subtitles.webm?controls=1&autoplay=1]] #+begin_notes You can find more details on our captioning process at emacsconf.org/captioning. I like using subed to edit subtitles within Emacs. #+end_notes * Crontab :PROPERTIES: :CREATED: [2023-10-14 Sat 12:55] :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :CUSTOM_ID: crontab :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab.opus" :AUDIO_DURATION_MS: 2726.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab.opus" :AUDIO_DURATION_MS: 2726.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Let's talk about actually playing the talks. #+end_notes ** Crontab :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-crontab.opus" :AUDIO_DURATION_MS: 19026.5 :END: +Using timers and TRAMP to play talks automatically+ Using crontab to play talks automatically #+begin_notes For EmacsConf 2022, we tried using Emacs timers to run the talks. It turns out that you can't call TRAMP from a timer when you're already using TRAMP from another timer at the same time. I thought about just tweaking the schedule so that we always start things at different times, but I figured there's probably a more elegant way to do this. #+end_notes ** Crontab format :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-crontab-format.opus" :AUDIO_DURATION_MS: 4006.5 :END: #+begin_example PATH=/usr/local/bin:/usr/bin MAILTO="" XDG_RUNTIME_DIR="/run/user/2002" 35 11 26 10 * /usr/bin/screen -dmS play-sat-open bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session sat-open | tee -a ~/track.log" 36 11 26 10 * /usr/bin/screen -dmS play-uni bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session uni | tee -a ~/track.log" 38 11 26 10 * /usr/bin/screen -dmS play-adventure bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session adventure | tee -a ~/track.log" #+end_example #+begin_notes This year, I'm planning to experiment with using cron to start talks on autopilot. #+end_notes ** handle-session :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-handle-session.opus" :AUDIO_DURATION_MS: 2886.5 :END: #+INCLUDE: ~/proj/emacsconf/emacsconf-ansible/roles/obs/templates/handle-session src sh #+begin_notes The shell scripts will take care of playing the videos... #+end_notes ** Q&A :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-q-a.opus" :AUDIO_DURATION_MS: 2046.5 :END: #+begin_src sh :eval no # ... from handle-session ... # Open the appropriate Q&A URL QA=$(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 $SLUG elif [ "$QA" != "null" ]; then /usr/local/bin/music & /usr/bin/firefox $QA # i3-msg 'layout splith' fi #+end_src #+begin_notes ... figuring out the appropriate Q&A... #+end_notes ** BigBlueButton :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-bigbluebutton.opus" :AUDIO_DURATION_MS: 2666.5 :END: #+INCLUDE: ~/proj/emacsconf/emacsconf-ansible/roles/obs/templates/bbb src sh #+begin_notes ... and joining the web conference if needed. #+end_notes ** Emacs Lisp :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-emacs-lisp.opus" :AUDIO_DURATION_MS: 2006.5 :END: [[defun:emacsconf-stream-format-crontab?bare=1]] #+begin_notes We just need to format the information... #+end_notes ** Installing the crontabs :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-installing-the-crontabs.opus" :AUDIO_DURATION_MS: 2086.5 :END: [[defun:emacsconf-stream-crontabs?bare=1]] #+begin_notes ...and install it as the track's crontab. #+end_notes ** Cancelling the crontabs :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab-cancelling-the-crontabs.opus" :AUDIO_DURATION_MS: 7726.5 :END: [[defun:emacsconf-stream-cancel-crontab?bare=1]] #+begin_notes It's useful to be able to switch tracks to manual mode independently, just in case things go haywire. Then we can start everything manually. #+end_notes * Transitions :PROPERTIES: :CUSTOM_ID: transitions :REVEAL_EXTRA_ATTR: data-audio-src="audio/transitions.opus" :AUDIO_DURATION_MS: 6206.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/transitions.opus" :AUDIO_DURATION_MS: 6206.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes I can also manually update a talk's status, like when the host tells me that it's okay to open up the Q&A. #+end_notes ** Updating the status via emacsclient :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/transitions-updating-the-status-via-emacsclient.opus" :AUDIO_DURATION_MS: 8986.5 :END: #+INCLUDE: ~/proj/emacsconf/emacsconf-ansible/roles/prerec/templates/talk src ansible #+begin_notes The shell scripts we run from the crontab can also update the talk status themselves. Then a bunch of things happen automatically based on the talk status changes. #+end_notes ** org-after-todo-state-change-hook :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/transitions-org-after-todo-state-change-hook.opus" :AUDIO_DURATION_MS: 6366.5 :END: [[defun:emacsconf-org-after-todo-state-change?bare=1][emacsconf-org-after-todo-state-change: Run all the hooks in ‘emacsconf-todo-hooks’.]] #+begin_notes This uses org-after-todo-state-change-hook. We get the talk information and pass it to a list of functions. #+end_notes * Announcing on IRC :PROPERTIES: :CUSTOM_ID: irc :REVEAL_EXTRA_ATTR: data-audio-src="audio/announcing-on-irc.opus" :AUDIO_DURATION_MS: 6346.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/announcing-on-irc.opus" :AUDIO_DURATION_MS: 6346.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Internet Relay Chat or IRC is an easy way for people to join the conversation around EmacsConf. #+end_notes ** Talk status changes :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/announcing-on-irc-talk-status-changes.opus" :AUDIO_DURATION_MS: 4486.5 :END: [[defun:emacsconf-erc-announce-on-change?bare=1][emacsconf-erc-announce-on-change: Announce talk.]] #+begin_notes We announce a talk whenever it changes state. #+end_notes ** Announcing talks on IRC :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/announcing-on-irc-announcing-talks-on-irc.opus" :AUDIO_DURATION_MS: 17846.5 :END: [[defun:erc-cmd-NOWPLAYING?bare=1]] #+begin_notes For example, when a talk starts, we post the URLs to the talk webpage and the Etherpad for questions. We change the topic as well, so anyone can see the current talk's information even if they're a little late. This is easy to do with a little bit of Emacs Lisp because (of course!) Emacs has an IRC client. #+end_notes * COMMENT Displaying a clock and a message :PROPERTIES: :CUSTOM_ID: clock-screen :REVEAL_EXTRA_ATTR: data-audio-src="audio/segment-1.m4a.opus" :END: =emacsconf-stream-display-clock-and-countdown= in [[emacsconf-el:emacsconf-stream.el]]: #+begin_src emacs-lisp :eval no (propertize (concat "Track: " emacsconf-stream-track "\n") 'face '(:height 400)) ;; ... (setq emacsconf-stream-clock-timer (run-at-time t 1 #'emacsconf-stream-update-time)) #+end_src #+begin_notes Sometimes we want to display announcements or timers, like the countdown to the next talk after lunch. Propertize makes it easy to display text with different properties. #+end_notes ** Timer :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/comment-displaying-a-clock-and-a-message-timer.opus" :AUDIO_DURATION_MS: 0 :END: [[defun:emacsconf-stream-update-time?bare=1]] #+begin_notes A timer lets us update it every second so that people know they're seeing things live and their stream hasn't frozen. #+end_notes * COMMENT Controlling the volume :PROPERTIES: :CREATED: [2023-10-14 Sat 14:53] :REVEAL_EXTRA_ATTR: data-audio-src="audio/comment-controlling-the-volume.opus" :AUDIO_DURATION_MS: 0 :END: We can normalize the audio volume for the recorded talks, but BigBlueButton sometimes does weird things with the audio volume of speakers who are doing live sessions. At the beginning of the Q&A session, then, we might need to adjust it to be louder or quieter depending on the feedback from people who are listening to the stream. We've set up our streaming so that the output audio from Firefox is actually a virtual device, which we can control from the command line. And since it can be controlled from the command line, then it can be adjusted from Emacs using a Hydra that uses TRAMP to run shell scripts remotely. * COMMENT checkin :PROPERTIES: :CREATED: [2023-10-14 Sat 14:49] :REVEAL_EXTRA_ATTR: data-audio-src="audio/comment-checkin.opus" :AUDIO_DURATION_MS: 0 :END: We email speakers their check-in details using the mail merge. Since most speakers will be handling their questions live, we ask them to check in via IRC an hour before their talk. That way we can get them settled into their BBB room if they're using one for questions. The track index that we have in our backstage area makes it easier to see what kind of Q&A is planned for which talk, where speakers should go, and what the check-in volunteer can confirm with the speaker while waiting. Once they're comfortable, they can go back to watching the conference. A few minutes before their Q&A starts, we drop by to give them a heads-up and remind them to close any tabs or applications they might be watching in, so they don't get feedback. Then the streamer joins them and they go. * COMMENT Archive :noexport: :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/comment-archive.opus" :AUDIO_DURATION_MS: 0 :END: ** Infrastructure overview :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/comment-archive-infrastructure-overview.opus" :AUDIO_DURATION_MS: 0 :END: The parts that I most need to revisit to make sure they work are: - publishing information to the wiki, especially during the conference - mail merge and moving the mail templates into the library - controlling both video streams from an Org file, especially if I can work around the TRAMP reentrant limitation - broadcasting announcements to IRC - redirecting to the Q&A room when the Q&A is opened #+begin_src dot :file ideas.png :cmdline "-Tdot | gvpr -cf ~/proj/graph/rankJustify.gvpr | neato -n -Tpng " digraph G { node [shape=box,fontname="Roboto",rankjustify="min"] rankdir=LR; info -> template; template -> wiki; template -> email; info -> images; info -> schedule -> {svg sort update email wiki}; "todo state change" -> TRAMP; {"play videos" "open Q&A"} -> TRAMP; info -> timers -> "clock and message"; timers -> "todo state change"; info -> navigation -> {completion "link types"}; completion -> embark; captions -> {email wiki} capture -> info; info -> "todo state change" -> "BBB redirect" "todo state change" -> {IRC "wiki update"} } #+end_src * Little by little :PROPERTIES: :CREATED: [2023-10-14 Sat 14:55] :REVEAL_EXTRA_ATTR: data-audio-src="audio/little-by-little.opus" :AUDIO_DURATION_MS: 6686.5 :END: #+begin_notes It seems like a lot of automation and Emacs Lisp, but really, all of this was just built up little by little. #+end_notes #+begin_comment I still don't have a lot of time. I'm mostly just squeezing this in on a laptop with a 14" screen, so that's why I tend to write small functions and then build them up. That way, when my train of thought inevitably gets derailed by a kiddo who wants to play with me, that's cool, I can pick things up again afterwards. #+end_comment * Lots of fun :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/lots-of-fun.opus" :AUDIO_DURATION_MS: 12706.5 :END: #+begin_notes And tinkering with this is *fun*, you know? It's like always being able to ask, "Hey, wouldn't it be cool if..." and then actually being able to go and do it. Sometimes it feels like EmacsConf is an excuse for me to play with Emacs. #+end_notes * Combining pieces :map: :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/combining-pieces.opus" :AUDIO_DURATION_MS: 22686.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes It's pretty amazing what you can do by combining a bunch of pieces. A way to store slightly-structured information. A way to get it out again. Templates. TRAMP, for working with remote files and running remote commands. A way to talk to a web browser. A way to work with SVGs. An email client. A chat client. You can smoosh them all together in a way that you couldn't if they were all separate things. #+end_notes * Links :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/links.opus" :AUDIO_DURATION_MS: 30657.0 :END: - https://git.emacsconf.org/emacsconf-el - https://emacsconf.org/2023/talks/emacsconf - What's next? - Other timezones? - Other types of things at EmacsConf? - Other things you can do with these ideas? Happy hacking! #+begin_notes The code is in the emacsconf-el repository. It's a bit of a tangle because it's accumulating organically and I haven't really had the brainspace to step back and clean it up. But if you spotted anything interesting in this presentation, you can go check it out and see what you can scavenge. The link and this presentation are available from this talk's webpage at https://emacsconf.org/2023/talks/emacsconf . Let's figure out how to make Emacsconf even awesomer next year! #+end_notes #+begin_comment I can't wait to see how EmacsConf will keep growing over the next few years. Imagine if we could get it to the point where people feel comfortable running EmacsConf in different timezones. I grew up in the Philippines, so I appreciate that timezones are tough! Imagine if we could make EmacsConf itself smooth enough so that we could add on other things, like panel conversations or workshops or clinics or hackathons. Could be fun, right? Let's figure out how to make EmacsConf even awesomer next year. And in the meantime, I'd love to see what you do with all these neat ideas. Let me know and I can put it into Emacs News. Happy hacking! #+end_comment