<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
	<title>Sacha Chua - category - video</title>
	<atom:link href="https://sachachua.com/blog/category/video/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/video" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/video/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Mon, 06 Apr 2026 14:36:57 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>#YayEmacs 9: Trimming/adding silences to get to a target; subed-record-sum-time</title>
		<link>https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 09 Jan 2025 15:24:29 GMT</pubDate>
    <category>audio</category>
<category>subed</category>
<category>yay-emacs</category>
<category>emacs</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/</guid>
		<description><![CDATA[<p>
New in this video: subed-record-sum-time, <code>#+PAD_LEFT</code> and <code>#+PAD_RIGHT</code>
</p>

<div class="row"><div class="columns"><div style="width: 400px"><video controls="1" src="https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/subed-record-sum-time.webm" poster="https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/2025-01-09_10-23-18.png" type="video/webm"><div>Video not supported. Thumbnail:<br><img src="https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/2025-01-09_10-23-18.png" alt="Thumbnail"></div><a href="https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/subed-record-sum-time.webm">Download the video</a></video></div></div><div class="columns">
<p>
I like the constraints of a one-minute video, so I added a subed-record-sum-time command. That way, when I edit the video using Emacs, I can check how long the result will be. First, I split the subtitles, align it with the audio to fix the timestamps, and double check the times. Then I can skip my oopses. Sometimes WhisperX doesn't catch them, so I also look at waveforms and characters per second. I already talk quickly, so I'm not going to speed that up but I can trim the pauses in between phrases which is easy to do with waveforms. Sometimes, after reviewing a draft, I realize I need a little more time. If the original audio has some silence, I can just copy and paste it. If not, I can pad left or pad right to add some silence. I can try the flow of some sections and compile the video when I'm ready. Emacs can do almost anything. Yay Emacs!
</p>

<p>You can <a href="https://youtube.com/watch?v=https://youtube.com/shorts/scyAC4yrWbE">watch this on YouTube</a>, <a href="https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/subed-record-sum-time.webm">download the video</a>, or <a href="https://sachachua.com/blog/feed/media/subed-record-sum-time.opus">download the audio</a>.</p></div></div>

<p>
Play by play:
</p>

<ul class="org-ul">
<li>I like the constraints of a one-minute video, so I added a <code>subed-record-sum-time</code> command. That way, when I edit the video using Emacs, I can check how long the result will be.
<ul class="org-ul">
<li><a href="https://github.com/sachac/subed-record">subed-record</a> uses subtitles and directives in
comments in a VTT subtitle file to edit audio
and video. <code>subed-record-sum-time</code> calculates
the resulting duration and displays it in the
minibuffer.</li>
</ul></li>
<li>First, I split the subtitles, align it with the audio to fix the timestamps, and double check the times.
<ul class="org-ul">
<li>I'm experimenting with an algorithmic way to
combine the breaks from my script with the
text from the transcript. <code>subed-align</code> calls
the <a href="https://github.com/readbeyond/aeneas">aeneas forced alignment tool</a> to match up
the text with the timestamps. I use
<code>subed-waveform-show-all</code> to show all the
waveforms.</li>
</ul></li>
<li>Then I can skip my oopses.
<ul class="org-ul">
<li>Adding a <code>NOTE #+SKIP</code> comment before a
subtitle makes <code>subed-record-compile-video</code>
and <code>subed-record-compile-flow</code> skip that part
of the audio.</li>
</ul></li>
<li>Sometimes WhisperX doesn't catch them,
<ul class="org-ul">
<li><a href="https://github.com/m-bain/whisperX">WhisperX</a> sometimes doesn't transcribe my false starts if I repeat things quickly.</li>
</ul></li>
<li>so I also look at waveforms
<ul class="org-ul">
<li><code>subed-waveform-show-all</code> adds waveforms for
all the subtitles. If I notice there's a pause
or a repeated shape in the waveform, or if I
listen and notice the repetition, I can
confirm by middle-clicking on the waveform to
sample part of it.</li>
</ul></li>
<li>and characters per second.
<ul class="org-ul">
<li>Low characters per second is sometimes a sign
that the timestamps are incorrect or there's a
repetition that wasn't transcribed.</li>
</ul></li>
<li>I already talk quickly, so I'm not going to speed that up
<ul class="org-ul">
<li>Also, I already sound like a chipmunk;
mechanically speeding up my recording to fit
in a certain time will make that worse =)</li>
</ul></li>
<li>but I can trim the pauses in between phrases which is easy to do with waveforms.
<ul class="org-ul">
<li>left-click to set the start, right-click to
set the stop. If I want to adjust the
previous/next one at the same time, I would
use shift-left-click or shift-right-click, but
here I want to skip the gaps between phrases,
so I adjust the current subtitle without
making the previous/next one longer.</li>
</ul></li>
<li>Sometimes, after reviewing a draft, I realize I need a little more time.
<ul class="org-ul">
<li>I can specify visuals like a video, animated
GIF, or an image by adding a <code>[[file:...]]</code>
link in the comment for a subtitle. That
visual will be used until the next visual is
specified in a comment on a different
subtitle. <code>subed-record-compile-video</code> can
automatically speed up video clips to fit in
the time for the current audio segment, which
is the set of subtitles before the next visual
is defined. After I compile and review the
video, sometimes I notice that something goes by too quickly.</li>
</ul></li>
<li>If the original audio has some silence, I can just copy and paste it.
<ul class="org-ul">
<li>This can sometimes feel more natural than adding in complete silence.</li>
</ul></li>
<li>If not, I can pad left or pad right to add some silence.
<ul class="org-ul">
<li>I added a new feature so that I could specify
something like <code>#+PAD_RIGHT: 1.5</code> in a comment
to add 1.5 seconds of silence after the audio
specified by that subtitle.</li>
</ul></li>
<li>I can try the flow of some sections
<ul class="org-ul">
<li>I can select a region and then use <code>M-x
    subed-record-compile-try-flow</code> to play the
audio or <code>C-u M-x
    subed-record-compile-try-flow</code> to play the
audio+video for that region.</li>
</ul></li>
<li>and compile the video when I'm ready.
<ul class="org-ul">
<li><code>subed-record-compile-video</code> compiles the
video to the file specified in <code>#+OUTPUT:
    filename</code>. ffmpeg is very arcane, so I'm glad
I can simplify my use of it with Emacs Lisp.</li>
</ul></li>
<li>Emacs can do almost anything. Yay Emacs!
<ul class="org-ul">
<li>Non-linear audio and video editing is actually
pretty fun in a text editor, especially when I
can just use <code>M-x vundo</code> to navigate my undo
history.</li>
</ul></li>
</ul>

<p>
Links:
</p>

<ul class="org-ul">
<li><a href="https://github.com/sachac/subed">sachac/subed: subed is a subtitle editor for Emacs</a></li>
<li><a href="https://github.com/sachac/subed-record">sachac/subed-record: Record audio in segments and compile it into a file</a></li>
<li><a href="https://github.com/m-bain/whisperX">m-bain/whisperX: WhisperX: Automatic Speech Recognition with Word-level Timestamps (&amp; Diarization)</a></li>
<li><a href="https://github.com/readbeyond/aeneas">readbeyond/aeneas: aeneas is a Python/C library and a set of tools to automagically synchronize audio and text (aka forced alignment)</a></li>
</ul>

<p>
Related:
</p>

<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2025/01/editing-videos-with-emacs-and-subed-record-el/">Editing videos with Emacs and subed-record.el</a></li>
<li><a href="https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/">Yay Emacs 5: Tweaking my video workflow with WhisperX and subed-record</a></li>
</ul>
<div><a href="https://sachachua.com/blog/2025/01/yayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time/index.org">View org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F01%2Fyayemacs-9-trimming-adding-silences-to-get-to-a-target-subed-record-sum-time%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Editing videos with Emacs and subed-record.el</title>
		<link>https://sachachua.com/blog/2025/01/editing-videos-with-emacs-and-subed-record-el/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Wed, 01 Jan 2025 19:23:04 GMT</pubDate>
    <category>emacs</category>
<category>subed</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/01/editing-videos-with-emacs-and-subed-record-el/</guid>
		<description><![CDATA[<p>
I want to document more of my Minecraft adventures with A+. Video is a natural way to do this. It builds on her familiarity with the tutorials and streams she enjoys watching. I set up <a href="https://obsproject.com/">OBS</a> on her laptop and plugged in my Blue Yeti microphone. We did our first interview yesterday. I edited and subtitled it (because why not!), uploaded it as an unlisted YouTube video, and shared it with her dad, sister, and cousins.
</p>

<p>
I did the video editing in Emacs with <a href="https://github.com/sachac/subed-record">subed-record</a>. First, I used <a href="https://github.com/m-bain/whisperX">WhisperX</a> to transcribe the video, and I used <a href="https://github.com/sachac/subed/blob/main/subed/subed-align.el">subed-align</a> to fix the timestamps with <a href="https://www.readbeyond.it/aeneas/">aeneas</a>. I normalized the audio with <a href="https://www.audacityteam.org/">Audacity</a> and I exported the .opus file for use in subed-record.el. Then I added <code>NOTE #+SKIP</code> before times I wanted to remove, like when she asked for a retake. Here's what that subtitle markup looks like:
</p>

<pre class="example" id="org5b67dfb">
WEBVTT

NOTE #+SKIP

00:00:00.000 &#45;&#45;&gt; 00:00:16.679
And then I'll record in my side also
and we'll just put it in somehow.
Somehow. Okay. We can edit that, right?
Yeah, we'll learn how to edit things.
It'll be great.

NOTE
Introduction
#+AUDIO: cuberventures-001.opus
[[file:intro.webm]]
#+OUTPUT: cuberventures-001-fxnt-create-2-windmill-home-cafe-trains-hotel-half-underwater.webm

00:00:16.680 &#45;&#45;&gt; 00:00:19.399
Okay, so now we're here with &lt;username&gt;.

00:00:19.400 &#45;&#45;&gt; 00:00:23.039
I want to find out what you like about Minecraft and

00:00:23.040 &#45;&#45;&gt; 00:00:26.079
all the cool things that you have been building lately.
</pre>

<p>
This was a little different from my usual video creation workflow, where I record the audio and the video separately. When I wrote subed-record.el, I assumed I'd edit the audio first,  choose images/GIFs/videos that were already ready to go, and then combine those visuals with sections of audio, speeding things up or slowing things down as needed. Now I wanted to apply the same edits to the video as I did to the audio. A+ did a great job of looking at stuff in Minecraft while talking about them, so I wanted to keep her narration in sync. I added some code to allow me to specify a same-edits keyword for the visuals. That meant that I would use the same selection list that I used for cutting the audio. Here's what that subtitle markup looks like:
</p>

<pre class="example" id="orgdbcad53">
NOTE
[[file:2024-12-31 10-35-14.mkv]]
#+OPTIONS: same-edits

00:00:43.860 &#45;&#45;&gt; 00:00:45.941
Shall we take a tour of my world?

00:00:45.942 &#45;&#45;&gt; 00:00:50.079
Sure, let's tell people which mod pack this is.

00:00:50.080 &#45;&#45;&gt; 00:00:55.639
This is FXNT Create 2, also known as FoxyNoTail Create 2.

NOTE Windmill

00:00:55.640 &#45;&#45;&gt; 00:00:58.239
I've got this little bit of path leading to the interview

00:00:58.240 &#45;&#45;&gt; 00:01:01.839
room. This is my unfinished windmill. I've been meaning to
</pre>

<p>
This workflow lets me cut out segments in the middle of the video, like this:
</p>

<pre class="example" id="orgd791344">
00:17:30.200 &#45;&#45;&gt; 00:17:33.119
great start for a tour. I'm looking forward to seeing what

00:17:33.120 &#45;&#45;&gt; 00:17:34.112
you will build next.

NOTE #+SKIP

00:17:34.113 &#45;&#45;&gt; 00:18:02.379
Do you have any last words before
we try to figure out this video editing thing?
Yeah. We'll cut that last part out.
Let's just do a retake on that last part.
Someday. Out here. Okay. There you go.
This is a beautiful view.

00:18:02.380 &#45;&#45;&gt; 00:18:08.119
The last things I want to say about this world is there'll be
</pre>

<p>
I also wanted to start the video with a segment from my recording, so we could see her avatar on screen during the introduction. She kept her computer on first-person POV instead of changing the camera. I used mpv to figure out the timestamps for the start and end of the part that I wanted to use, then I used ffmpeg to cut that clip. I added a comment with a link to that video in order to use it before the main video. That's the <code>[[file:intro.webm]]</code> in the first section's comments.
</p>

<p>
After testing a small section of the transcript by selecting a region and using <code>subed-record-compile-video</code>, I deselected the region and used <code>subed-record-compile-video</code> to produce the whole video.
</p>

<p>
I also modified <code>subed-record-compile-subtitles</code>
to include the other non-directive comments, so I
can include the section headings in the raw VTT
file and have them turn up in the exported
version. Then I can use the new
<code>subed-section-comments-as-chapters</code> command to
copy those as chapters for the YouTube
description.
</p>

<p>
We're not going to share that particular video
yet, but I'm looking forward to trying that
technique with videos about stuff I'm figuring out
in Minecraft or Emacs. It's also tempting me to
think about ways to specify transitions like
crossfades and other fancy effects like overlays.
</p>

<p>
I like using the transcript as the starting point
for video editing. It just makes sense to me to
work with it as text. I also like this experiment
with documenting more of our Minecraft
experimentation. It seems to get her talking and
encourages her to build more. I'm looking forward
to learning more about Minecraft and making videos
too.
</p>

<p>
We did another video today using the new shortcuts
I've just set up for toggling OBS recording. This
time we didn't even need to do any editing. I used
Org Export to make her a little HTML file that had
the two videos on it, so she can review it any
time. Onward!
</p>
<div><a href="https://sachachua.com/blog/2025/01/editing-videos-with-emacs-and-subed-record-el/index.org">View org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F01%2Fediting-videos-with-emacs-and-subed-record-el%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js</title>
		<link>https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 11 Jan 2024 17:25:40 GMT</pubDate>
    <category>emacs</category>
<category>drawing</category>
<category>org</category>
<category>ffmpeg</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/</guid>
		<description><![CDATA[<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-types">Getting the sketches: PDFs are not all the same</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-paths-in-order">Animation style 1: displaying paths in order</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-identify-paths">Identifying paths from Supernote sketches</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-sorting">Sorting and animating the paths by IDs</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-highlights">Animation style 2: Building up a map with temporary highlights</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-animation-next">Ideas for next steps</a></li>
</ul>
</div>

<div class="tldr" id="org2916c84">
<p>
tldr (2167 words): I can make animating presentation maps easier by
writing my own functions for the Emacs text editor. In this post, I
show how I can animate an SVG element by element. I can also add IDs
to the path and use CSS to build up an SVG with temporary highlighting
in a Reveal.js presentation.
</p>

</div>

<p>
</p><div class="sketch-full"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2024-01-10-01%20Animating%20SVG%20topic%20maps%20with%20Inkscape%2C%20Emacs%2C%20FFmpeg%2C%20and%20Reveal.js%20%23drawing%20%23emacs%20%23video%20%23workflow.png" data-src="https://sketches.sachachua.com/static/2024-01-10-01%20Animating%20SVG%20topic%20maps%20with%20Inkscape%2C%20Emacs%2C%20FFmpeg%2C%20and%20Reveal.js%20%23drawing%20%23emacs%20%23video%20%23workflow.png" data-title="2024-01-10-01 Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js #drawing #emacs #video #workflow.png" data-w="2808" data-h="3744"><picture>
      <img src="https://sketches.sachachua.com/static/2024-01-10-01%20Animating%20SVG%20topic%20maps%20with%20Inkscape%2C%20Emacs%2C%20FFmpeg%2C%20and%20Reveal.js%20%23drawing%20%23emacs%20%23video%20%23workflow.png" width="2808" height="3744" alt="2024-01-10-01 Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js #drawing #emacs #video #workflow.png" loading="lazy" style="max-height: 90vw; height: auto; width: auto" decoding="async">
      <figcaption>2024-01-10-01 Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js #drawing #emacs #video #workflow.png</figcaption>
    </picture></a></div>
<p></p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Text from the sketch</strong></summary>
<ul class="org-ul">
<li>PNG: Inkscape: trace</li>
<li>Supernote (e-ink)</li>
<li>iPad: Adobe Fresco</li>
</ul>

<p>
Convert PDF to SVG with Inkscape (Cairo option) or pdftocairo)
</p>

<ul class="org-ul">
<li>PNG / Supernote PDF: Combined shapes. Process
<ol class="org-ol">
<li>Break apart, fracture overlaps</li>
<li>Recombine</li>
<li>Set IDs</li>
<li>Sort paths -&gt; Animation style 1</li>
</ol></li>

<li>Adobe Fresco: individual elements in order; landscape feels natural</li>
</ul>

<p>
Animation styles
</p>

<ul class="org-ul">
<li>Animation style 1: Display elements one after another</li>
<li>Animation style 2: Display elements one after another, and also show/hide highlights
<ul class="org-ul">
<li>Table: slide ID, IDs to add, temporary highlights -&gt; Reveal.js: CSS with transitions</li>
</ul></li>
</ul>

<p>
Ideas for next steps:
</p>

<ul class="org-ul">
<li>Explore graphviz &amp; other diagramming tools</li>
<li>Frame-by-frame SVGs
<ul class="org-ul">
<li>on include</li>
<li>write to files</li>
</ul></li>
<li>FFmpeg crossfade</li>
<li>Recording Reveal.js presentations</li>
<li>Use OCR results?</li>
</ul>


</details>

<p>
I often have a hard time organizing my thoughts into a linear
sequence. Sketches are nice because they let me jump around and still
show the connections between ideas. For presentations, I'd like to
walk people through these sketches by highlighting different areas.
For example, I might highlight the current topic or show the previous
topics that are connected to the current one. Of course, this is
something Emacs can help with. Before we dive into it, here are quick
previews of the kinds of animation I'm talking about:
</p>


<figure id="org9c4a1c6">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation-loop.gif" alt="animation-loop.gif">

<figcaption><span class="figure-number">Figure 1: </span>Animation style 1: based on drawing order</figcaption>
</figure>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm">Download the video</a></video><figcaption>Animation style 2: building up a map with temporary highlights</figcaption></figure>
<p></p>
<div id="outline-container-svg-types" class="outline-2">
<h3 id="svg-types">Getting the sketches: PDFs are not all the same</h3>
<div class="outline-text-2" id="text-svg-types">
<p>
Let's start with getting the sketches. I usually export my sketches as
PNGs from my <a href="https://supernote.com/">Supernote</a> A5X. But if I know that I'm going to animate a
sketch, I can export it as a PDF. I've recently been experimenting
with Adobe Fresco on the iPad, which can also export to PDF. The PDF I
get from Fresco is easier to animate, but I prefer to draw on the
Supernote because it's an e-ink device (and because the kiddo usually
uses the iPad).
</p>

<p>
If I start with a PNG, I could use Inkscape to trace the PNG and turn
it into an SVG. I think Inkscape uses autotrace behind the scenes. I
don't usually put my highlights on a separate layer, so autotrace will
make odd shapes.
</p>

<p>
It's a lot easier if you start off with vector graphics in the first
place. I can export a vector PDF from the SuperNote A5X and either
import it into Inkscape using the Cairo option or use the command-line
pdftocairo tool.
</p>

<p>
I've been looking into using Adobe Fresco, which is a free app
available for the iPad. Fresco's PDF export can be converted to an SVG
using Inkscape or PDF to Cairo. What I like about the output of this
app is that it gives me individual elements as their own paths and
they're listed in order of drawing. This makes it really easy to
animate by just going through the paths in order.
</p>
</div>
</div>
<div id="outline-container-svg-paths-in-order" class="outline-2">
<h3 id="svg-paths-in-order">Animation style 1: displaying paths in order</h3>
<div class="outline-text-2" id="text-svg-paths-in-order">
<p>
Here's a sample SVG file that pdfcairo creates from an Adobe Fresco
PDF export:
</p>


<div class="org-src-container">
<pre class="src src-sh">pdftocairo -svg ~/Downloads/subed-audio.pdf ~/Downloads/subed-audio.svg
</pre>
</div>


<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Sample SVG</strong></summary>

<figure id="org6cff3aa">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/subed-audio.svg" alt="subed-audio.svg" class="org-svg">

</figure>


</details>

<p>
Adobe Fresco also includes built-in time-lapse, but since I often like
to move things around or tidy things up, it's easier to just work with
the final image, export it as a PDF, and convert it to an SVG.
</p>

<p>
I can make a very simple animation by setting the opacity of all the
paths to 0, then looping through the elements to set the opacity back
to 1 and write that version of the SVG to a separate file.
From <a href="https://sachachua.com/dotemacs#how-can-i-generate-png-frames-that-step-through-the-highlights">how-can-i-generate-png-frames-that-step-through-the-highlights</a>:
</p>

<p>
</p><details><summary>my-animate-svg-paths: Add one path at a time. Save the resulting SVGs to OUTPUT-DIR.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-animate-svg-paths</span> (filename output-dir)
  <span class="org-doc">"Add one path at a time. Save the resulting SVGs to OUTPUT-DIR."</span>
  (<span class="org-keyword">unless</span> (file-directory-p output-dir)
    (make-directory output-dir t))
  (<span class="org-keyword">let*</span> ((dom (xml-parse-file filename))
         (paths (seq-filter (<span class="org-keyword">lambda</span> (e) (<span class="org-keyword">dom-attr</span> e <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>))
                            (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>)))
         (total (length paths))
         (frame-num (length paths))
         result)
    (<span class="org-keyword">dolist</span> (elem paths)
      (dom-set-attribute elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
                         (concat
                          (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
                          <span class="org-string">";mix-blend-mode:darken"</span>)))
    (<span class="org-keyword">with-temp-file</span> (expand-file-name (format <span class="org-string">"frame-%03d.svg"</span> (1+ frame-num)) output-dir)
      (xml-print dom))
    (<span class="org-keyword">dolist</span> (elem paths)
      (dom-set-attribute elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
                         (concat
                          (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
                          <span class="org-string">";fill-opacity:0"</span>)))
    (<span class="org-keyword">dolist</span> (elem paths)
      (<span class="org-keyword">with-temp-file</span> (expand-file-name
                       (format <span class="org-string">"frame-%03d.svg"</span>
                               (- total frame-num))
                       output-dir)
        (message <span class="org-string">"%03d"</span> frame-num)
        (dom-set-attribute elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
                           (concat (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
                                   <span class="org-string">";fill-opacity:1"</span>))
        (<span class="org-keyword">push</span> (list (format <span class="org-string">"frame-%03d.svg"</span>
                            (1+ (- total frame-num)))
                    (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>))
              result)
        (<span class="org-keyword">setq</span> frame-num (1- frame-num))
        (xml-print dom)))
    (reverse result)))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#svg-animating-paths-in-order">Context</a></div>
<p></p>

<p>
Here's how I call it:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(my-animate-svg-paths <span class="org-string">"~/Downloads/subed-audio.svg"</span> <span class="org-string">"/tmp/subed-audio/frames"</span> t)
</pre>
</div>


<p>
Then I can use FFmpeg to combine all of those frames into a video:
</p>


<div class="org-src-container">
<pre class="src src-sh">ffmpeg -i frame-%03d.svg -vf palettegen -y palette.png
ffmpeg -framerate 30 -i frame-%03d.svg -i palette.png -lavfi <span class="org-string">"paletteuse"</span> -loop 0 -y animation-loop.gif
</pre>
</div>



<figure id="orgc3765a8">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation-loop.gif" alt="animation-loop.gif">

<figcaption><span class="figure-number">Figure 2: </span>Animating SVG paths based on drawing order</figcaption>
</figure>

<p>
Neither Supernote nor Adobe Fresco give me the original stroke
information. These are filled shapes, so I can't animate something
drawing it. But having different elements appear in sequence is fine
for my purposes. If you happen to know how to get stroke information
out of Supernote .note files or of an iPad app that exports nice
single-line SVGs that have stroke direction, I would love to hear
about it.
</p>
</div>
</div>
<div id="outline-container-svg-identify-paths" class="outline-2">
<h3 id="svg-identify-paths">Identifying paths from Supernote sketches</h3>
<div class="outline-text-2" id="text-svg-identify-paths">
<p>
When I export a PDF from Supernote and convert it to an SVG, each
color is a combined shape with all the elements. If I want to animate
parts of the image, I have to break it up and recombine selected
elements (Inkscape's Ctrl-k shortcut) so that the holes in shapes are
properly handled. This is a bit of a tedious process and it usually
ends up with elements in a pretty random order. Since I have to
reorder elements by hand, I don't really want to animate the sketch
letter-by-letter. Instead, I combine them into larger chunks like
topics or paragraphs.
</p>

<p>
The following code takes the PDF, converts it to an SVG, recolours
highlights, and then breaks up paths into elements:
</p>

<p>
</p><details><summary>my-sketch-convert-pdf-and-break-up-paths: Convert PDF to SVG and break up paths.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-sketch-convert-pdf-and-break-up-paths</span> (pdf-file <span class="org-type">&amp;optional</span> rotate)
  <span class="org-doc">"Convert PDF to SVG and break up paths."</span>
  (<span class="org-keyword">interactive</span> (list (read-file-name
                      (format <span class="org-string">"PDF (%s): "</span>
                              (my-latest-file <span class="org-string">"~/Dropbox/Supernote/EXPORT/"</span> <span class="org-string">"pdf"</span>))
                      <span class="org-string">"~/Dropbox/Supernote/EXPORT/"</span>
                      (my-latest-file <span class="org-string">"~/Dropbox/Supernote/EXPORT/"</span> <span class="org-string">"pdf"</span>)
                      t
                      nil
                      (<span class="org-keyword">lambda</span> (s) (string-match <span class="org-string">"pdf"</span> s)))))
  (<span class="org-keyword">unless</span> (file-exists-p (concat (file-name-sans-extension pdf-file) <span class="org-string">".svg"</span>))
    (call-process <span class="org-string">"pdftocairo"</span> nil nil nil <span class="org-string">"-svg"</span> (expand-file-name pdf-file)
                  (expand-file-name (concat (file-name-sans-extension pdf-file) <span class="org-string">".svg"</span>))))
  (<span class="org-keyword">let</span> ((dom (xml-parse-file (expand-file-name (concat (file-name-sans-extension pdf-file) <span class="org-string">".svg"</span>))))
        highlights)
    (<span class="org-keyword">setq</span> highlights (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">g</span> <span class="org-highlight-quoted-quote">'</span>((id . <span class="org-string">"highlights"</span>))))
    (dom-append-child dom highlights)
    (<span class="org-keyword">dolist</span> (path (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>))
      <span class="org-comment-delimiter">;;  </span><span class="org-comment">recolor and move</span>
      (<span class="org-keyword">unless</span> (string-match (regexp-quote <span class="org-string">"rgb(0%,0%,0%)"</span>) (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>) <span class="org-string">""</span>))
        (dom-remove-node dom path)
        (dom-append-child highlights path)
        (dom-set-attribute
         path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
         (replace-regexp-in-string
          (regexp-quote <span class="org-string">"rgb(78.822327%,78.822327%,78.822327%)"</span>)
          <span class="org-string">"#f6f396"</span>
          (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>) <span class="org-string">""</span>))))
      (<span class="org-keyword">let</span> ((parent (dom-parent dom path)))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">break apart</span>
        (<span class="org-keyword">when</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>)
          (<span class="org-keyword">dolist</span> (part (split-string (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>) <span class="org-string">"M "</span> t <span class="org-string">" +"</span>))
            (dom-append-child
             parent
             (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span> <span class="org-highlight-quoted-quote">`</span>((style . ,(<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>))
                               (d . ,(concat <span class="org-string">"M "</span> part))))))
          (dom-remove-node dom path))))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">remove the use</span>
    (<span class="org-keyword">dolist</span> (use (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">use</span>))
      (dom-remove-node dom use))
    (<span class="org-keyword">dolist</span> (use (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">image</span>))
      (dom-remove-node dom use))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">move the first g down</span>
    (<span class="org-keyword">let</span> ((g (car (dom-by-id dom <span class="org-string">"surface1"</span>))))
      (<span class="org-keyword">setf</span> (cddar dom)
            (seq-remove (<span class="org-keyword">lambda</span> (o)
                          (<span class="org-keyword">and</span> (listp o) (string= (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"surface1"</span>)))
                        (dom-children dom)))
      (dom-append-child dom g)
      (<span class="org-keyword">when</span> rotate
        (<span class="org-keyword">let*</span> ((old-width (<span class="org-keyword">dom-attr</span> dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">width</span>))
               (old-height (<span class="org-keyword">dom-attr</span> dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">height</span>))
               (view-box (mapcar <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">string-to-number</span> (split-string (<span class="org-keyword">dom-attr</span> dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">viewBox</span>))))
               (rotate (format <span class="org-string">"rotate(90) translate(0 %s)"</span> (- (elt view-box 3)))))
          (dom-set-attribute dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">width</span> old-height)
          (dom-set-attribute dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">height</span> old-width)
          (dom-set-attribute dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">viewBox</span> (format <span class="org-string">"0 0 %d %d"</span> (elt view-box 3) (elt view-box 2)))
          (dom-set-attribute highlights <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">transform</span> rotate)
          (dom-set-attribute g <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">transform</span> rotate))))
    (<span class="org-keyword">with-temp-file</span> (expand-file-name (concat (file-name-sans-extension pdf-file) <span class="org-string">"-split.svg"</span>))
      (svg-print (car dom)))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#how-can-i-generate-png-frames-that-step-through-the-highlights">Context</a></div>
<p></p>


<figure id="orgc788582">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2023-10-split.svg" alt="2023-10-split.svg" class="org-svg">

<figcaption><span class="figure-number">Figure 3: </span>Image after splitting up into elements</figcaption>
</figure>

<p>
You can see how the spaces inside letters like "o" end up being black.
Selecting and combining those paths fixes that.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.16.48.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.16.48.webm">Download the video</a></video><figcaption>Combining paths in Inkscape</figcaption></figure>
<p></p>

<p>
If there were shapes that were touching, then I need to draw lines and
fracture the shapes in order to break them apart.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.19.17.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.19.17.webm">Download the video</a></video><figcaption>Fracturing shapes and checking the highlights</figcaption></figure>
<p></p>

<p>
The end result should be an SVG with the different chunks that I might
want to animate, but I need to identify the paths first. You can
assign object IDs in Inkscape, but this is a bit of an annoying
process since I haven't figured out a keyboard-friendly way to set
object IDs. I usually find it easier to just set up an Autokey
shortcut (or AutoHotkey in Windows) to click on the ID text box so
that I can type something in.
</p>

<details><summary>Autokey script for clicking</summary>
<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> time
<span class="org-variable-name">x</span>, <span class="org-variable-name">y</span> <span class="org-operator">=</span> mouse.get_location()
<span class="org-comment-delimiter"># </span><span class="org-comment">Use the coordinates of the ID text field on your screen; xev can help</span>
mouse.click_absolute(3152, 639, 1)
time.sleep(1)
keyboard.send_keys(<span class="org-string">"&lt;ctrl&gt;+a"</span>)
mouse.move_cursor(x, y)
</pre>
</div>

</details>

<p>
Then I can select each element, press the shortcut key, and type an ID
into the textbox. I might use "t-&#x2026;" to indicate the text for a map
section, "h-&#x2026;" to indicate a highlight, and arrows by specifying
their start and end.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_06.01.49.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_06.01.49.webm">Download the video</a></video><figcaption>Setting IDs in Inkscape</figcaption></figure>
<p></p>

<p>
To simplify things, I wrote a function in Emacs that will go through
the different groups that I've made, show each path in a different
color and with a reasonable guess at a bounding box, and prompt me for
an ID. This way, I can quickly assign IDs to all of the paths. The
completion is mostly there to make sure I don't accidentally reuse an
ID, although it can try to combine paths if I specify the ID. It saves
the paths after each change so that I can start and stop as needed.
Identifying paths in Emacs is usually much nicer than identifying them
in Inkscape.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_07.33.50.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_07.33.50.webm">Download the video</a></video><figcaption>Identifying paths inside Emacs</figcaption></figure>
<p></p>

<p>
</p><details><summary>my-svg-identify-paths: Prompt for IDs for each path in FILENAME.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-svg-identify-paths</span> (filename)
  <span class="org-doc">"Prompt for IDs for each path in FILENAME."</span>
  (<span class="org-keyword">interactive</span> (list (read-file-name <span class="org-string">"SVG: "</span> nil nil
                                     (<span class="org-keyword">lambda</span> (f) (string-match <span class="org-string">"\\.svg$"</span> f)))))
  (<span class="org-keyword">let*</span> ((dom (car (xml-parse-file filename)))
         (paths (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>))
         (vertico-count 3)
         (ids (seq-keep (<span class="org-keyword">lambda</span> (path)
                          (<span class="org-keyword">unless</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
                            (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>)))
                        paths))
         (edges (window-inside-pixel-edges (get-buffer-window)))
         id)
    (my-svg-display <span class="org-string">"*image*"</span> dom nil t)
    (<span class="org-keyword">dolist</span> (path paths)
      (<span class="org-keyword">when</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">display the image with an outline</span>
        (<span class="org-keyword">unwind-protect</span>
            (<span class="org-keyword">progn</span>
              (my-svg-display <span class="org-string">"*image*"</span> dom (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) t)
              (<span class="org-keyword">setq</span> id (completing-read
                        (format <span class="org-string">"ID (%s): "</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>))
                        ids))
              <span class="org-comment-delimiter">;; </span><span class="org-comment">already exists, merge with existing element</span>
              (<span class="org-keyword">if-let</span> ((old (dom-by-id dom id)))
                  (<span class="org-keyword">progn</span>
                    (dom-set-attribute
                     old
                     <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>
                     (concat (<span class="org-keyword">dom-attr</span> (dom-by-id dom id) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>)
                             <span class="org-string">" "</span>
                             <span class="org-comment-delimiter">;; </span><span class="org-comment">change relative to absolute</span>
                             (replace-regexp-in-string <span class="org-string">"^m"</span> <span class="org-string">"M"</span>
                                                       (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>))))
                    (dom-remove-node dom path)
                    (<span class="org-keyword">setq</span> id nil))
                (dom-set-attribute path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> id)
                (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ids</span> id))))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">save the image just in case we get interrupted halfway through</span>
        (<span class="org-keyword">with-temp-file</span> filename
          (svg-print dom))))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#svg-identifying-paths">Context</a></div>
<p></p>
</div>
</div>
<div id="outline-container-svg-sorting" class="outline-2">
<h3 id="svg-sorting">Sorting and animating the paths by IDs</h3>
<div class="outline-text-2" id="text-svg-sorting">
<p>
Then I can animate SVGs by specifying the IDs. I can reorder the paths
in the SVG itself so that I can animate it group by group, like the
way that the Adobe Fresco SVGs were animated element by element.
</p>

<details><summary>Reordering paths</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org2c2010e">(my-svg-reorder-paths <span class="org-string">"~/proj/2023-12-audio-workflow/map.svg"</span>
                      <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"t-start"</span> <span class="org-string">"h-audio"</span> <span class="org-string">"h-capture"</span> <span class="org-string">"t-but"</span> <span class="org-string">"t-mic"</span> <span class="org-string">"h-mic"</span>
                        <span class="org-string">"t-reviewing"</span> <span class="org-string">"h-reviewing"</span>
                        <span class="org-string">"t-words"</span> <span class="org-string">"h-words"</span> <span class="org-string">"t-workflow"</span> <span class="org-string">"h-workflow"</span>
                        <span class="org-string">"t-lapel"</span> <span class="org-string">"h-lapel"</span>
                        <span class="org-string">"mic-recorder"</span> <span class="org-string">"t-recorder"</span> <span class="org-string">"h-recorder"</span>
                        <span class="org-string">"t-syncthing"</span> <span class="org-string">"h-sync"</span>
                        <span class="org-string">"t-keywords"</span> <span class="org-string">"h-keywords"</span> <span class="org-string">"t-keyword-types"</span>
                        <span class="org-string">"t-lines"</span> <span class="org-string">"h-lines"</span>
                        <span class="org-string">"t-align"</span> <span class="org-string">"h-align"</span>
                        <span class="org-string">"arrow"</span>
                        <span class="org-string">"t-org"</span> <span class="org-string">"h-org"</span> <span class="org-string">"t-todo"</span> <span class="org-string">"h-todo"</span> <span class="org-string">"h-linked"</span>
                        <span class="org-string">"t-jump"</span> <span class="org-string">"h-jump"</span>
                        <span class="org-string">"t-waveform"</span> <span class="org-string">"h-waveform"</span>
                        <span class="org-string">"t-someday"</span>
                        <span class="org-string">"h-sections"</span>
                        <span class="org-string">"t-speech-recognition"</span> <span class="org-string">"h-speech-recognition"</span>
                        <span class="org-string">"t-ai"</span> <span class="org-string">"h-ai"</span>
                        <span class="org-string">"t-summary"</span>
                        <span class="org-string">"extra"</span>)
                      <span class="org-string">"~/proj/2023-12-audio-workflow/map-output.svg"</span>)
(my-animate-svg-paths <span class="org-string">"~/proj/2023-12-audio-workflow/map-output.svg"</span>
                      <span class="org-string">"~/proj/2023-12-audio-workflow/frames/"</span>)
</pre>
</div>

</details>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Table of filenames after reordering paths and animating the image</strong></summary>
<table>


<colgroup>
<col class="org-left">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left">frame-001.svg</td>
<td class="org-left">t-start</td>
</tr>

<tr>
<td class="org-left">frame-002.svg</td>
<td class="org-left">h-audio</td>
</tr>

<tr>
<td class="org-left">frame-003.svg</td>
<td class="org-left">h-capture</td>
</tr>

<tr>
<td class="org-left">frame-004.svg</td>
<td class="org-left">t-but</td>
</tr>

<tr>
<td class="org-left">frame-005.svg</td>
<td class="org-left">t-mic</td>
</tr>

<tr>
<td class="org-left">frame-006.svg</td>
<td class="org-left">h-mic</td>
</tr>

<tr>
<td class="org-left">frame-007.svg</td>
<td class="org-left">t-reviewing</td>
</tr>

<tr>
<td class="org-left">frame-008.svg</td>
<td class="org-left">h-reviewing</td>
</tr>

<tr>
<td class="org-left">frame-009.svg</td>
<td class="org-left">t-words</td>
</tr>

<tr>
<td class="org-left">frame-010.svg</td>
<td class="org-left">h-words</td>
</tr>

<tr>
<td class="org-left">frame-011.svg</td>
<td class="org-left">t-workflow</td>
</tr>

<tr>
<td class="org-left">frame-012.svg</td>
<td class="org-left">h-workflow</td>
</tr>

<tr>
<td class="org-left">frame-013.svg</td>
<td class="org-left">t-lapel</td>
</tr>

<tr>
<td class="org-left">frame-014.svg</td>
<td class="org-left">h-lapel</td>
</tr>

<tr>
<td class="org-left">frame-015.svg</td>
<td class="org-left">mic-recorder</td>
</tr>

<tr>
<td class="org-left">frame-016.svg</td>
<td class="org-left">t-recorder</td>
</tr>

<tr>
<td class="org-left">frame-017.svg</td>
<td class="org-left">h-recorder</td>
</tr>

<tr>
<td class="org-left">frame-018.svg</td>
<td class="org-left">t-syncthing</td>
</tr>

<tr>
<td class="org-left">frame-019.svg</td>
<td class="org-left">h-sync</td>
</tr>

<tr>
<td class="org-left">frame-020.svg</td>
<td class="org-left">t-keywords</td>
</tr>

<tr>
<td class="org-left">frame-021.svg</td>
<td class="org-left">h-keywords</td>
</tr>

<tr>
<td class="org-left">frame-022.svg</td>
<td class="org-left">t-keyword-types</td>
</tr>

<tr>
<td class="org-left">frame-023.svg</td>
<td class="org-left">t-lines</td>
</tr>

<tr>
<td class="org-left">frame-024.svg</td>
<td class="org-left">h-lines</td>
</tr>

<tr>
<td class="org-left">frame-025.svg</td>
<td class="org-left">t-align</td>
</tr>

<tr>
<td class="org-left">frame-026.svg</td>
<td class="org-left">h-align</td>
</tr>

<tr>
<td class="org-left">frame-027.svg</td>
<td class="org-left">arrow</td>
</tr>

<tr>
<td class="org-left">frame-028.svg</td>
<td class="org-left">t-org</td>
</tr>

<tr>
<td class="org-left">frame-029.svg</td>
<td class="org-left">h-org</td>
</tr>

<tr>
<td class="org-left">frame-030.svg</td>
<td class="org-left">t-todo</td>
</tr>

<tr>
<td class="org-left">frame-031.svg</td>
<td class="org-left">h-todo</td>
</tr>

<tr>
<td class="org-left">frame-032.svg</td>
<td class="org-left">h-linked</td>
</tr>

<tr>
<td class="org-left">frame-033.svg</td>
<td class="org-left">t-jump</td>
</tr>

<tr>
<td class="org-left">frame-034.svg</td>
<td class="org-left">h-jump</td>
</tr>

<tr>
<td class="org-left">frame-035.svg</td>
<td class="org-left">t-waveform</td>
</tr>

<tr>
<td class="org-left">frame-036.svg</td>
<td class="org-left">h-waveform</td>
</tr>

<tr>
<td class="org-left">frame-037.svg</td>
<td class="org-left">t-someday</td>
</tr>

<tr>
<td class="org-left">frame-038.svg</td>
<td class="org-left">h-sections</td>
</tr>

<tr>
<td class="org-left">frame-039.svg</td>
<td class="org-left">t-speech-recognition</td>
</tr>

<tr>
<td class="org-left">frame-040.svg</td>
<td class="org-left">h-speech-recognition</td>
</tr>

<tr>
<td class="org-left">frame-041.svg</td>
<td class="org-left">t-ai</td>
</tr>

<tr>
<td class="org-left">frame-042.svg</td>
<td class="org-left">h-ai</td>
</tr>

<tr>
<td class="org-left">frame-043.svg</td>
<td class="org-left">t-summary</td>
</tr>

<tr>
<td class="org-left">frame-044.svg</td>
<td class="org-left">extra</td>
</tr>
</tbody>
</table>


</details>

<p>
The table of filenames makes it easy to use specific frames as part of
a presentation or video.
</p>

<p>
Here is the result as a video:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((compile-media-output-video-width 1280)
      (compile-media-output-video-height 720))
  (my-ffmpeg-animate-images
   (directory-files <span class="org-string">"~/proj/2023-12-audio-workflow/frames/"</span> t <span class="org-string">"\\.svg$"</span>)
   (expand-file-name <span class="org-string">"~/proj/2023-12-audio-workflow/frames/animation.webm"</span>)
   4))
</pre>
</div>


<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation.webm">Download the video</a></video><figcaption>Animation of SVG by paths</figcaption></figure>
<p></p>

<p>
The way it works is that the <code>my-svg-reorder-paths</code> function removes
and readds elements following the list of IDs specified, so
everything's ready to go for step-by-step animation. Here's the code:
</p>

<p>
</p><details><summary>my-svg-reorder-paths: Sort paths in FILENAME.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-svg-reorder-paths</span> (filename <span class="org-type">&amp;optional</span> ids output-filename)
  <span class="org-doc">"Sort paths in FILENAME."</span>
  (<span class="org-keyword">interactive</span> (list (read-file-name <span class="org-string">"SVG: "</span> nil nil (<span class="org-keyword">lambda</span> (f) (string-match <span class="org-string">"\\.svg$"</span> f)))
                     nil (read-file-name <span class="org-string">"Output: "</span>)))
  (<span class="org-keyword">let*</span> ((dom (car (xml-parse-file filename)))
         (paths (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>))
         (parent (dom-parent dom (car paths)))
         (ids-left
          (nreverse (seq-keep (<span class="org-keyword">lambda</span> (path)
                                (<span class="org-keyword">unless</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
                                  (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>)))
                              paths)))
         list)
    (<span class="org-keyword">when</span> (called-interactively-p)
      (<span class="org-keyword">while</span> ids-left
        (my-svg-display <span class="org-string">"*image*"</span> dom (car ids-left))
        (<span class="org-keyword">let</span> ((current (completing-read
                        (format <span class="org-string">"ID (%s): "</span>
                                (car ids-left))
                        ids-left nil nil nil nil (car ids-left)))
              node)
          (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ids</span> current)
          (<span class="org-keyword">setq</span> ids-left (seq-remove (<span class="org-keyword">lambda</span> (o) (string= o current)) ids-left)))))
    (<span class="org-keyword">if</span> ids <span class="org-comment-delimiter">;; </span><span class="org-comment">reorganize under the first path's parent</span>
        (<span class="org-keyword">progn</span>
          (<span class="org-keyword">dolist</span> (id ids)
            (<span class="org-keyword">if-let</span> ((node (car (dom-by-id dom id))))
                (<span class="org-keyword">progn</span>
                  (dom-remove-node dom node)
                  (dom-append-child parent node))
              (message <span class="org-string">"Could not find %s"</span> id)))
          (<span class="org-keyword">with-temp-file</span> (<span class="org-keyword">or</span> output-filename filename)
            (svg-print dom))))
    (nreverse (seq-keep (<span class="org-keyword">lambda</span> (path)
                          (<span class="org-keyword">unless</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
                            (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>)))
                        (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>)))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#svg-sorting-paths">Context</a></div>
<p></p>
</div>
</div>
<div id="outline-container-svg-highlights" class="outline-2">
<h3 id="svg-highlights">Animation style 2: Building up a map with temporary highlights</h3>
<div class="outline-text-2" id="text-svg-highlights">
<p>
I can also use CSS rules to transition between opacity values for more
complex animations. For my <a href="https://emacsconf.org/2023/talks/emacsconf">EmacsConf 2023 presentation</a>, I wanted to
make <a href="https://sachachua.com/proj/emacsconf-2023-emacsconf/#/sec-title-slide">a self-paced, narrated presentation</a> so that people could follow
hyperlinks, read the source code, and explore. I wanted to include a
<a href="https://sachachua.com/proj/emacsconf-2023-emacsconf/#/slide-props-map">map</a> so that I could try to make sense of everything. For this map, I
wanted to highlight the previous sections that were connected to the
topic for the current section.
</p>

<p>
I used a custom Org link to include the full contents of the SVG
instead of just including it with an img tag.
</p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+ATTR_HTML: :class r-stretch</span>
<span class="org-org-link">my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html</span>
</pre>
</div>


<p>
</p><details><summary>my-include-export: Export PATH to FORMAT using the specified wrap parameter.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-include-export</span> (path _ format _)
  <span class="org-doc">"Export PATH to FORMAT using the specified wrap parameter."</span>
  (<span class="org-keyword">let</span> (params body start end)
    (<span class="org-keyword">when</span> (string-match <span class="org-string">"^</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.*+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(?:</span></span><span class="org-string">::</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string">\\?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.*+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">"</span> path)
      (<span class="org-keyword">setq</span> params (<span class="org-keyword">save-match-data</span> (org-protocol-convert-query-to-plist (match-string 2 path)))
            path (match-string 1 path)))
    (<span class="org-keyword">with-temp-buffer</span>
      (insert-file-contents-literally path)
      (<span class="org-keyword">when</span> (string-match <span class="org-string">"\\.org$"</span> path)
        (org-mode))
      (<span class="org-keyword">if</span> (plist-get params <span class="org-builtin">:name</span>)
          (<span class="org-keyword">when</span> (org-babel-find-named-block (plist-get params <span class="org-builtin">:name</span>))
            (goto-char (org-babel-find-named-block (plist-get params <span class="org-builtin">:name</span>)))
            (<span class="org-keyword">let</span> ((block (org-element-context)))
              (<span class="org-keyword">setq</span> start (org-element-begin block)
                    end (org-element-end block))))
        (goto-char (point-min))
        (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:from-regexp</span>)
          (re-search-forward (url-unhex-string (plist-get params <span class="org-builtin">:from-regexp</span>)))
          (goto-char (match-beginning 0)))
        (<span class="org-keyword">setq</span> start (point))
        (<span class="org-keyword">setq</span> end (point-max))
        (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:to-regexp</span>)
          (re-search-forward (url-unhex-string (plist-get params <span class="org-builtin">:to-regexp</span>)))
          (<span class="org-keyword">setq</span> end (match-beginning 0))))
      (<span class="org-keyword">setq</span> body (buffer-substring start end)))
    (<span class="org-keyword">with-temp-buffer</span>
      (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:wrap</span>)
        (<span class="org-keyword">let*</span> ((wrap (plist-get params <span class="org-builtin">:wrap</span>))
               block args)
          (<span class="org-keyword">when</span> (string-match <span class="org-string">"\\&lt;</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">\\S-+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string"> +.*</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">?"</span> wrap)
            (<span class="org-keyword">setq</span> block (match-string 1 wrap))
            (<span class="org-keyword">setq</span> args (match-string 2 wrap))
            (<span class="org-keyword">setq</span> body (format <span class="org-string">"#+BEGIN_%s%s\n%s\n#+END_%s\n"</span>
                               block (<span class="org-keyword">or</span> args <span class="org-string">""</span>)
                               body
                               block)))))
      (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:summary</span>)
        (<span class="org-keyword">setq</span> body (format <span class="org-string">"#+begin_my_details %s\n%s\n#+end_my_details\n"</span>
                           (plist-get params <span class="org-builtin">:summary</span>)
                           body)))
      (insert body)
      (org-export-as format nil nil t))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#org-mode-including-portions-of-files-between-two-regular-expressions">Context</a></div>
<p></p>

<p>
I wanted to be able to specify the entire sequence using a table in
the Org Mode source for my presentation. Each row had the slide ID, a
list of highlights in the form <code>prev1,prev2;current</code>, and a
comma-separated list of elements to add to the full-opacity view.
</p>

<div class="bordered" id="org9d17b2b">
<table id="org67db231">


<colgroup>
<col class="org-left">

<col class="org-left">

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Slide</th>
<th scope="col" class="org-left">Highlight</th>
<th scope="col" class="org-left">Additional elements</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">props-map</td>
<td class="org-left">h-email;h-properties</td>
<td class="org-left">t-email,email-properties,t-properties</td>
</tr>

<tr>
<td class="org-left">file-prefixes</td>
<td class="org-left">h-properties;h-filename</td>
<td class="org-left">t-filename,properties-filename</td>
</tr>

<tr>
<td class="org-left">renaming</td>
<td class="org-left">h-filename;h-renaming</td>
<td class="org-left">t-renaming,filename-renaming</td>
</tr>

<tr>
<td class="org-left">shell-scripts</td>
<td class="org-left">h-renaming;h-shell-scripts</td>
<td class="org-left">renaming-shell-scripts,t-shell-scripts</td>
</tr>

<tr>
<td class="org-left">availability</td>
<td class="org-left">h-properties;h-timezone</td>
<td class="org-left">t-timezone,properties-timezone</td>
</tr>

<tr>
<td class="org-left">schedule</td>
<td class="org-left">h-timezone;h-schedule</td>
<td class="org-left">t-schedule,timezone-schedule</td>
</tr>

<tr>
<td class="org-left">emailing-speakers</td>
<td class="org-left">h-timezone,h-mail-merge;h-emailing-speakers</td>
<td class="org-left">schedule-emailing-speakers,t-emailing-speakers</td>
</tr>

<tr>
<td class="org-left">template</td>
<td class="org-left">h-properties;h-template</td>
<td class="org-left">t-template,properties-template</td>
</tr>

<tr>
<td class="org-left">wiki</td>
<td class="org-left">h-template;h-wiki</td>
<td class="org-left">t-wiki,template-wiki,schedule-wiki</td>
</tr>

<tr>
<td class="org-left">pad</td>
<td class="org-left">h-template;h-pad</td>
<td class="org-left">template-pad,t-pad</td>
</tr>

<tr>
<td class="org-left">mail-merge</td>
<td class="org-left">h-template;h-mail-merge</td>
<td class="org-left">t-mail-merge,template-mail-merge,schedule-mail-merge,emailing-speakers-mail-merge</td>
</tr>

<tr>
<td class="org-left">bbb</td>
<td class="org-left">h-bbb</td>
<td class="org-left">t-bbb</td>
</tr>

<tr>
<td class="org-left">checkin</td>
<td class="org-left">h-mail-merge;h-checkin</td>
<td class="org-left">t-checkin,bbb-checkin</td>
</tr>

<tr>
<td class="org-left">redirect</td>
<td class="org-left">h-bbb;h-redirect</td>
<td class="org-left">t-redirect,bbb-redirect</td>
</tr>

<tr>
<td class="org-left">shortcuts</td>
<td class="org-left">h-email;h-shortcuts</td>
<td class="org-left">t-shortcuts,email-shortcuts</td>
</tr>

<tr>
<td class="org-left">logbook</td>
<td class="org-left">h-shortcuts;h-logbook</td>
<td class="org-left">shortcuts-logbook,t-logbook</td>
</tr>

<tr>
<td class="org-left">captions</td>
<td class="org-left">h-captions</td>
<td class="org-left">t-captions,captions-wiki</td>
</tr>

<tr>
<td class="org-left">tramp</td>
<td class="org-left">h-captions;h-tramp</td>
<td class="org-left">t-tramp,captions-tramp</td>
</tr>

<tr>
<td class="org-left">crontab</td>
<td class="org-left">h-tramp;h-crontab</td>
<td class="org-left">tramp-crontab,bbb-crontab,t-crontab</td>
</tr>

<tr>
<td class="org-left">transitions</td>
<td class="org-left">h-crontab;h-transitions</td>
<td class="org-left">shell-scripts-transitions,t-transitions,shortcuts-transitions,transitions-crontab</td>
</tr>

<tr>
<td class="org-left">irc</td>
<td class="org-left">h-transitions;h-irc</td>
<td class="org-left">t-irc,transitions-irc</td>
</tr>
</tbody>
</table>

</div>

<p>
Reveal.js adds a "current" class to the slide, so I can use that as a
trigger for the transition. I have a bit of Emacs Lisp code that
generates some very messy CSS, in which I specify the ID of the slide,
followed by all of the elements that need their opacity set to 1, and
also specifying the highlights that will be shown in an animated way.
</p>

<p>
</p><details><summary>my-reveal-svg-progression-css: Make the CSS.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-progression-css</span> (map-progression <span class="org-type">&amp;optional</span> highlight-duration)
  <span class="org-doc">"Make the CSS.</span>
<span class="org-doc">map-progression should be a list of lists with the following format:</span>
<span class="org-doc">((\"slide-id\" \"prev1,prev2;cur1\" \"id-to-add1,id-to-add2\") ...)."</span>
  (<span class="org-keyword">setq</span> highlight-duration (<span class="org-keyword">or</span> highlight-duration 2))
  (<span class="org-keyword">let</span> (full)
    (format
     <span class="org-string">"&lt;style&gt;%s&lt;/style&gt;"</span>
     (mapconcat
      (<span class="org-keyword">lambda</span> (slide)
        (<span class="org-keyword">setq</span> full (append (split-string (elt slide 2) <span class="org-string">","</span>) full))
        (format <span class="org-string">"#slide-%s.present path { opacity: 0.2 }</span>
<span class="org-string">%s { opacity: 1 !important }</span>
<span class="org-string">%s"</span>
                (car slide)
                (mapconcat (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
                           full
                           <span class="org-string">", "</span>)
                (my-reveal-svg-highlight-different-colors slide)))
      map-progression
      <span class="org-string">"\n"</span>))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#reveal-js-sketch-animation">Context</a></div>
I call it from my Org file like this:
<p></p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+NAME: progression-css</span>
<span class="org-org-block-begin-line">#+begin_src emacs-lisp :exports code :var map-progression=progression :var highlight-duration=2 :results silent</span>
<span class="org-org-block">(my-reveal-svg-progression-css map-progression highlight-duration)</span>
<span class="org-org-block-end-line">#+end_src</span>
</pre>
</div>


<p>
Here's an excerpt showing the kind of code it makes:
</p>

<pre class="example" id="orgd2b216e">
&lt;style&gt;#slide-props-map.present path { opacity: 0.2 }
#slide-props-map.present #t-email, #slide-props-map.present #email-properties, #slide-props-map.present #t-properties { opacity: 1 !important }
#slide-props-map.present #h-email { fill: #c6c6c6; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.0s }#slide-props-map.present #h-properties { fill: #f6f396; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.5s }
#slide-file-prefixes.present path { opacity: 0.2 }
#slide-file-prefixes.present #t-filename, #slide-file-prefixes.present #properties-filename, #slide-file-prefixes.present #t-email, #slide-file-prefixes.present #email-properties, #slide-file-prefixes.present #t-properties { opacity: 1 !important }
#slide-file-prefixes.present #h-properties { fill: #c6c6c6; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.0s }#slide-file-prefixes.present #h-filename { fill: #f6f396; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.5s }
...&lt;/style&gt;
</pre>

<p>
Since it's automatically generated, I don't have to worry about it
once I've gotten it to work. It's all hidden in a
results drawer. So this CSS highlights specific parts of the SVG with
a transition, and the highlight changes over the course of a second or
two. It highlights the previous names and then the current one. The
topics I'd already discussed would be in black, and the topics that I
had yet to discuss would be in very light gray. This could give people
a sense of the progress through the presentation.
</p>

<p>
</p><details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Code for making the CSS</strong></summary>

<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org1ab8883">(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-animation</span> (slide)
  (string-join
   (seq-map-indexed
    (<span class="org-keyword">lambda</span> (step-ids i)
      (format <span class="org-string">"%s { fill: #f6f396; transition: fill %ds; transition-delay: %ds }"</span>
              (mapconcat
               (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
               (split-string step-ids <span class="org-string">","</span>)
               <span class="org-string">", "</span>)
              highlight-duration
              (* i highlight-duration)))
    (split-string (elt slide 1) <span class="org-string">";"</span>))
   <span class="org-string">"\n"</span>))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-highlight-different-colors</span> (slide)
  (<span class="org-keyword">let*</span> ((colors <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"#f6f396"</span> <span class="org-string">"#c6c6c6"</span>)) <span class="org-comment-delimiter">; </span><span class="org-comment">reverse</span>
         (steps (split-string (elt slide 1) <span class="org-string">";"</span>))
         (step-length 0.5))
    (string-join
     (seq-map-indexed
      (<span class="org-keyword">lambda</span> (step-ids i)
        (format <span class="org-string">"%s { fill: %s; opacity: 1 !important; transition: fill %.1fs; transition-delay: %.1fs }"</span>
                (mapconcat
                 (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
                 (split-string step-ids <span class="org-string">","</span>)
                 <span class="org-string">", "</span>)
                (elt colors (- (length steps) i 1))
                step-length
                (* i 0.5)))
      steps))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-progression-css</span> (map-progression <span class="org-type">&amp;optional</span> highlight-duration)
  <span class="org-doc">"Make the CSS.</span>
<span class="org-doc">map-progression should be a list of lists with the following format:</span>
<span class="org-doc">((\"slide-id\" \"prev1,prev2;cur1\" \"id-to-add1,id-to-add2\") ...)."</span>
  (<span class="org-keyword">setq</span> highlight-duration (<span class="org-keyword">or</span> highlight-duration 2))
  (<span class="org-keyword">let</span> (full)
    (format
     <span class="org-string">"&lt;style&gt;%s&lt;/style&gt;"</span>
     (mapconcat
      (<span class="org-keyword">lambda</span> (slide)
        (<span class="org-keyword">setq</span> full (append (split-string (elt slide 2) <span class="org-string">","</span>) full))
        (format <span class="org-string">"#slide-%s.present path { opacity: 0.2 }</span>
<span class="org-string">%s { opacity: 1 !important }</span>
<span class="org-string">%s"</span>
                (car slide)
                (mapconcat (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
                           full
                           <span class="org-string">", "</span>)
                (my-reveal-svg-highlight-different-colors slide)))
      map-progression
      <span class="org-string">"\n"</span>))))
</pre>
</div>




</details>

<p></p>

<p>
As a result, as I go through my presentation, the image appears to
build up incrementally, which is the effect that I was going for.
I can test this by exporting only my map slides:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">save-excursion</span>
  (goto-char (org-babel-find-named-block <span class="org-string">"progression-css"</span>))
  (org-babel-execute-src-block))
(<span class="org-keyword">let</span> ((org-tags-exclude-from-inheritance <span class="org-string">"map"</span>)
      (org-export-select-tags <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"map"</span>)))
   (oer-reveal-export-to-html))
</pre>
</div>


<p>
<video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm">Download the video</a></video>
</p>
</div>
</div>
<div id="outline-container-svg-animation-next" class="outline-2">
<h3 id="svg-animation-next">Ideas for next steps</h3>
<div class="outline-text-2" id="text-svg-animation-next">
<ul class="org-ul">
<li>Graphviz, mermaid-js, and other diagramming tools can make SVGs. I
should be able to adapt my code to animate those diagrams by adding
other elements in addition to <code>path</code>. Then I'll be able to make
diagrams even more easily.</li>

<li>Since SVGs can contain CSS, I could make an SVG equivalent of the
CSS rules I used for the presentation, maybe calling a function with
a Lisp expression that specifies the operations (ex:
<code>("frame-001.svg" "h-foo" opacity 1)</code>). Then I could write frames to
SVGs.</li>

<li>FFmpeg has a crossfade filter. With a little bit of figuring out, I
should be able to make the same kind of animation in a webm form
that I can include in my regular videos instead of using Reveal.js
and CSS transitions.</li>

<li>I've also been thinking about automating the recording of my
Reveal.js presentations. For my EmacsConf talk, I opened my
presentation, started the recording with the system audio and the
screen, and then let it autoplay the presentation. I checked on it
periodically to avoid the screensaver/energy saving things from
kicking in and so that I could stop the recording when it's
finished. If I want to make this take less work, one option is to
use ffmpeg's "-t" argument to specify the expected duration of the
presentation so that I don't have to manually stop it. I'm also
thinking about using Puppeteer to open the presentation, check when
it's fully loaded, and start the process to record it - maybe even
polling to see whether it's finished. I haven't gotten around to it
yet. Anyhow, those are some ideas to explore next time.</li>

<li>As for animation, I'm still curious about the possibility of
finding a way to access the raw stroke information if it's even
available from my Supernote A5X (difficult because it's a
proprietary data format) or finding an app for the iPad that exports
single line SVGs that use stroke information instead of fill. That
would only be if I wanted to do those even fancier animations that
look like the whole thing is being drawn for you. I was trying to
figure out if I could green screen the Adobe Fresco timelapse videos
so that even if I have a pre-sketch to figure out spacing and remind
me what to draw, I can just export the finished elements. But
there's too much anti-aliasing and I haven't figured out how to do
it cleanly yet. Maybe some other day.</li>

<li>I use Google Cloud Vision's text detection engine to convert my
handwriting to text. It can give me bounding polygons for words or
paragraphs. I might be able to figure out which curves are entirely
within a word's bounding polygon and combine those automatically.</li>

<li>It would be pretty cool if I could combine the words recognized by
Google Cloud Vision with the word-level timestamps from speech
recognition so that I could get word-synced sketchnote animations
with maybe a little manual intervention.</li>
</ul>

<p>
Anyway, those are some workflows for animating sketches with Inkscape
and Emacs. Yay Emacs!
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/index.org">View org source for this post</a></div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F01%2Fanimating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Quick notes on livestreaming to YouTube with FFmpeg on a Lenovo X230T</title>
		<link>https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 04 Jan 2024 15:57:25 GMT</pubDate>
    <category>video</category>
<category>youtube</category>
<category>streaming</category>
<category>ffmpeg</category>
<category>yay-emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/</guid>
		<description><![CDATA[<div class="update" id="org78e2751">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2024-01-05 Fri]</span></span>: Updated scripts
</p>

</div>

<p>
</p><div class="sketch-full"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" data-src="https://sketches.sachachua.com/static/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" data-title="2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png" data-w="2808" data-h="3744"><picture>
      <img src="https://sketches.sachachua.com/static/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" width="2808" height="3744" alt="2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png" loading="lazy" style="max-height: 90vw; height: auto; width: auto" decoding="async">
      <figcaption>2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png</figcaption>
    </picture></a></div>
<p></p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Text from the sketch</strong></summary>
<p>
Quick thoughts on livestreaming
</p>

<p>
Why:
</p>

<ul class="org-ul">
<li>work out loud</li>
<li>share tips</li>
<li>share more</li>
<li>spark conversations</li>
<li>(also get questions about things)</li>
</ul>

<p>
Doable with ffmpeg on my X230T:
</p>

<ul class="org-ul">
<li>streaming from my laptop</li>
<li>lapel mic + system audio,</li>
<li>second screen for monitoring</li>
</ul>

<p>
Ideas for next time:
</p>

<ul class="org-ul">
<li>Overall notes in Emacs with outline, org-timer timestamped notes; capture to this file</li>
<li>Elisp to start/stop the stream → find old code</li>
<li>Use the Yeti? Better sound</li>
<li>tee to a local recording</li>
<li>grab screenshot from SuperNote mirror?</li>
</ul>

<p>
Live streaming info density:
</p>

<ul class="org-ul">
<li>High: Emacs News review, package/workflow demo</li>
<li>Narrating a blog post to make it a video</li>
<li>Categorizing Emacs News, exploring packages</li>
<li>Low: Figuring things out</li>
</ul>

<p>
YouTube can do closed captions for livestreams, although accuracy is
low. Videos take a while to be ready to download.
</p>


</details>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud">Experimenting with working out loud</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud">
<p>
I wanted to write <a href="https://emacsconf.org/2023/report">a report on EmacsConf 2023</a> so that we could share it
with speakers, volunteers, participants, donors, related organizations
like the Free Software Foundation, and other communities. I
experimented with livestreaming via YouTube while I worked on the
conference highlights.
</p>

<p>
It's a little over an hour long and probably very boring, but it was
nice of people to drop by and say hello.
</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/KXKA-3JFb14?si=2V5XQmQSUte0ZWyF" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<p>
The main parts are:
</p>

<ul class="org-ul">
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h0m00s">0:00</a>: reading through other conference reports for inspiration</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h6m54s">6:54</a>: writing an overview of the talks</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h13m10s">13:10</a>: adding quotes for specific talks</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h25m00s">25:00</a>: writing about the overall conference</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h32m00s">32:00</a>: squeezing in more highlights</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h49m00s">49:00</a>: fiddling with the formatting and the export</li>
</ul>

<p>
It mostly worked out, aside from a brief moment of "uhhh, I'm
looking at our private conf.org file on stream". Fortunately, the
e-mail addresses that were showed were the public ones.
</p>
</div>
</div>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details">Technical details</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details">
<p>
Setup:
</p>

<ul class="org-ul">
<li><p>
I set up environment variables and screen resolution:
</p>

<div class="org-src-container">
<pre class="src src-sh" id="org8d1d9a1">  <span class="org-comment-delimiter"># </span><span class="org-comment">From pacmd list-sources | egrep '^\s+name'</span>
  <span class="org-variable-name">LAPEL</span>=alsa_input.usb-Jieli_Technology_USB_Composite_Device_433035383239312E-00.mono-fallback <span class="org-comment-delimiter">#</span>
  <span class="org-variable-name">YETI</span>=alsa_input.usb-Blue_Microphones_Yeti_Stereo_Microphone_REV8-00.analog-stereo
  <span class="org-variable-name">SYSTEM</span>=alsa_output.pci-0000_00_1b.0.analog-stereo.monitor
  <span class="org-comment-delimiter"># </span><span class="org-comment">MIC=$LAPEL</span>
  <span class="org-comment-delimiter"># </span><span class="org-comment">AUDIO_WEIGHTS="1 1"</span>
  <span class="org-variable-name">MIC</span>=$<span class="org-variable-name">YETI</span>
  <span class="org-variable-name">AUDIO_WEIGHTS</span>=<span class="org-string">"0.5 0.5"</span>
  <span class="org-variable-name">OFFSET</span>=+1920,430
  <span class="org-variable-name">SIZE</span>=1280x720
  <span class="org-variable-name">SCREEN</span>=LVDS-1  <span class="org-comment-delimiter"># </span><span class="org-comment">from xrandr</span>
  xrandr &#45;&#45;output $<span class="org-variable-name">SCREEN</span> &#45;&#45;mode 1280x720
</pre>
</div>
</li>
<li>I switch to a larger size and a light theme. I also turn <a href="https://github.com/minad/consult">consult</a> previews off to minimize the risk of leaking data through buffer previews.
<details open=""><summary>my-emacsconf-prepare-for-screenshots: Set the resolution, change to a light theme, and make the text bigger.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacsconf-prepare-for-screenshots</span> ()
  (<span class="org-keyword">interactive</span>)
  (shell-command <span class="org-string">"xrandr &#45;&#45;output LVDS-1 &#45;&#45;mode 1280x720"</span>)
  (modus-themes-load-theme <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-operandi</span>)
  (my-hl-sexp-update-overlay)
  (set-face-attribute <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> nil <span class="org-builtin">:height</span> 170)
  (keycast-mode))
</pre></div></details></li>
</ul>

<p>
Testing:
</p>


<div class="org-src-container">
<pre class="src src-sh">ffmpeg -f x11grab -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -y /tmp/test.png; display /tmp/test.png
ffmpeg -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-variable-name">amix</span>=<span class="org-variable-name">inputs</span>=2:<span class="org-variable-name">weights</span>=$<span class="org-variable-name">AUDIO_WEIGHTS</span>:<span class="org-variable-name">duration</span>=longest:<span class="org-variable-name">normalize</span>=0 -y /tmp/test.mp3; mpv /tmp/test.mp3
<span class="org-variable-name">DATE</span>=$(date <span class="org-string">"+%Y-%m-%d-%H-%M-%S"</span>)
ffmpeg -f x11grab -framerate 30 -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-string">"amix=inputs=2:weights=$AUDIO_WEIGHTS:duration=longest:normalize=0"</span> -c:v libx264 -preset fast -maxrate 690k -bufsize 2000k -g 60 -vf <span class="org-variable-name">format</span>=yuv420p -c:a aac -b:a 96k -y -flags +global_header <span class="org-string">"/home/sacha/recordings/$DATE.flv"</span> -f flv
</pre>
</div>


<p>
Streaming:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">DATE</span>=$(date <span class="org-string">"+%Y-%m-%d-%H-%M-%S"</span>)
ffmpeg -f x11grab -framerate 30 -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-string">"amix=inputs=2:weights=$AUDIO_WEIGHTS:duration=longest:normalize=0[audio]"</span> -c:v libx264 -preset fast -maxrate 690k -bufsize 2000k -g 60 -vf <span class="org-variable-name">format</span>=yuv420p -c:a aac -b:a 96k -y -f tee -map 0:v -map <span class="org-string">'[audio]'</span> -flags +global_header  <span class="org-string">"/home/sacha/recordings/$DATE.flv|[f=flv]rtmp://a.rtmp.youtube.com/live2/$YOUTUBE_KEY"</span>
</pre>
</div>


<p>
To restore my previous setup:
</p>

<p>
</p><details open=""><summary>my-emacsconf-back-to-normal: Go back to a more regular setup.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacsconf-back-to-normal</span> ()
  (<span class="org-keyword">interactive</span>)
  (shell-command <span class="org-string">"xrandr &#45;&#45;output LVDS-1 &#45;&#45;mode 1366x768"</span>)
  (modus-themes-load-theme <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-vivendi</span>)
  (my-hl-sexp-update-overlay)
  (set-face-attribute <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> nil <span class="org-builtin">:height</span> 115)
  (keycast-mode -1))
</pre></div></details>
<p></p>
</div>
</div>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps">Ideas for next steps</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps">
<p>
I can think of a few workflow tweaks that might be fun:
</p>

<ul class="org-ul">
<li>a stream notes buffer on the right side of the screen for context
information, timestamped notes to make editing/review easier (maybe
using <code>org-timer</code>), etc. I experimented with some streaming-related
code in my config, so I can dust that off and see what that's like.
I also want to have an <code>org-capture</code> template for it so that I can add
notes from anywhere.</li>
<li>a quick way to <a href="https://sachachua.com/blog/2024/01/using-puppeteer-to-grab-an-image-from-the-supernote-s-screen-mirror/">add a screenshot from my Supernote</a> to my Org files</li>
</ul>

<p>
I think I'll try going through an informal presentation or Emacs News as my next livestream experiment, since that's probably higher information density.
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/index.org">View org source for this post</a></div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F01%2Fquick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>EmacsConf backstage: Trimming the BigBlueButton recordings based on YouTube duration</title>
		<link>https://sachachua.com/blog/2023/12/emacsconf-backstage-trimming-the-bigbluebutton-recordings/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 28 Dec 2023 20:04:29 GMT</pubDate>
    <category>emacsconf</category>
<category>emacs</category>
<category>youtube</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/12/emacsconf-backstage-trimming-the-bigbluebutton-recordings/</guid>
		<description><![CDATA[<p>
I wanted to get the Q&amp;A sessions up quickly after the conference, so I
uploaded them to YouTube and added them to the <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUdrW3JkugtKv8xPelUoOyP">EmacsConf 2023
playlist</a>. I used YouTube's video editor to roughly guess where to
trim them based on the waveforms. I needed to actually trim the source
videos, though, so that our copies would be up to date and I could use
those for the Toobnix uploads.
</p>

<p>
My first task was to figure out which videos needed to be trimmed to
match the YouTube edits. First, I retrieved the video details using
the API and the code that I added to <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-extract.el">emacsconf-extract.el</a>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-video-details (emacsconf-extract-youtube-get-video-details emacsconf-extract-youtube-api-playlist-items))
</pre>
</div>


<p>
Then I made a table comparing the file duration with the YouTube duration, showing rows only if the difference was more than 3 minutes.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp" id="orgfb43101">(append
 <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"type"</span> <span class="org-string">"slug"</span> <span class="org-string">"file duration"</span> <span class="org-string">"youtube duration"</span> <span class="org-string">"diff"</span>))
 (<span class="org-keyword">let</span> ((threshold-secs (* 3 60))) <span class="org-comment-delimiter">; </span><span class="org-comment">don't sweat small differences</span>
   (seq-mapcat
    (<span class="org-keyword">lambda</span> (talk)
      (seq-keep
       (<span class="org-keyword">lambda</span> (row)
         (<span class="org-keyword">when</span> (plist-get talk (cadr row))
           (<span class="org-keyword">let*</span> ((video (emacsconf-extract-youtube-find-url-video-in-list
                          (plist-get talk (cadr row))
                          emacsconf-extract-youtube-api-video-details))
                  (video-duration (<span class="org-keyword">if</span> (<span class="org-keyword">and</span> video (emacsconf-extract-youtube-duration-msecs video))
                                      (/ (emacsconf-extract-youtube-duration-msecs video) 1000.0)))
                  (file-duration (ceiling
                                  (/ (compile-media-get-file-duration-ms (emacsconf-talk-file talk (format <span class="org-string">"&#45;&#45;%s.webm"</span> (car row))))
                                     1000.0))))
             (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> video-duration (&gt; (abs (- file-duration video-duration)) threshold-secs))
               (list (car row)
                     (plist-get talk <span class="org-builtin">:slug</span>)
                     (<span class="org-keyword">and</span> file-duration (format-seconds <span class="org-string">"%h:%z%.2m:%.2s"</span> file-duration))
                     (<span class="org-keyword">and</span> video-duration (format-seconds <span class="org-string">"%h:%z%.2m:%.2s"</span> video-duration))
                     (emacsconf-format-seconds
                      (abs (- file-duration video-duration))))))))
       <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"main"</span> <span class="org-builtin">:youtube-url</span>)
         (<span class="org-string">"answers"</span> <span class="org-builtin">:qa-youtube-url</span>))))
    (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))
</pre>
</div>


<p>
Then I got the commands to trim the videos.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"> (mapconcat (<span class="org-keyword">lambda</span> (row)
              (<span class="org-keyword">let</span> ((talk (emacsconf-resolve-talk (elt row 1))))
                (format <span class="org-string">"ffmpeg -y -i %s&#45;&#45;%s.webm -t %s -c copy %s&#45;&#45;%s&#45;&#45;trimmed.webm"</span>
                        (plist-get talk <span class="org-builtin">:file-prefix</span>)
                        (car row)
                        (concat (elt row 3) <span class="org-string">".000"</span>)
                        (plist-get talk <span class="org-builtin">:file-prefix</span>)
                        (car row))))
            (cdr to-trim)
            <span class="org-string">"\n"</span>))
</pre>
</div>


<p>
After quickly checking the results, I copied them over to the original videos, updated the video data in my conf.org, and republished the info pages in the wiki.
</p>

<p>
The time I spent on figuring out how to talk to the YouTube API feels like it's paying off.
</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Femacsconf-backstage-trimming-the-bigbluebutton-recordings%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>EmacsConf backstage: Using Spookfox to publish YouTube and Toobnix video drafts</title>
		<link>https://sachachua.com/blog/2023/12/emacsconf-backstage-using-spookfox-to-publish-youtube-and-toobnix-video-drafts/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 12 Dec 2023 20:09:03 GMT</pubDate>
    <category>emacsconf</category>
<category>emacs</category>
<category>spookfox</category>
<category>youtube</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/12/emacsconf-backstage-using-spookfox-to-publish-youtube-and-toobnix-video-drafts/</guid>
		<description><![CDATA[<p>
I ran into quota limits when uploading videos to YouTube with a command-line tool, so I uploaded videos by selecting up to 15 videos at a time using the web-based interface. Each video was a draft, though, and I was having a hard time updating its visibility through the API. I think it eventually worked, but in the meantime, I used this very hacky hack to look for the "Edit Draft" button and click through the screens to publish them.
</p>

<p>
</p><details open=""><summary>emacsconf-extract-youtube-publish-video-drafts-with-spookfox: Look for drafts and publish them.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-publish-video-drafts-with-spookfox</span> ()
  <span class="org-doc">"Look for drafts and publish them."</span>
  (<span class="org-keyword">while</span> (not (eq (spookfox-js-injection-eval-in-active-tab
                   <span class="org-string">"document.querySelector('.edit-draft-button div') != null"</span> t) <span class="org-builtin"><span class="org-warning">:false</span></span><span class="org-warning">))</span>
    (<span class="org-keyword">progn</span>
      (spookfox-js-injection-eval-in-active-tab
       <span class="org-string">"document.querySelector('.edit-draft-button div').click()"</span> t)
      (sleep-for 2)
      (spookfox-js-injection-eval-in-active-tab
       <span class="org-string">"document.querySelector('#step-title-3').click()"</span> t)
      (<span class="org-keyword">when</span> (spookfox-js-injection-eval-in-active-tab
             <span class="org-string">"document.querySelector('tp-yt-paper-radio-button[name=\"PUBLIC\"] #radioLabel').click()"</span> t)
        (spookfox-js-injection-eval-in-active-tab
         <span class="org-string">"document.querySelector('#done-button').click()"</span> t)
        (<span class="org-keyword">while</span> (not (eq  (spookfox-js-injection-eval-in-active-tab
                          <span class="org-string">"document.querySelector('#close-button .label') == null"</span> t)
                         <span class="org-builtin">:false</span>))
          (sleep-for 1))

        (spookfox-js-injection-eval-in-active-tab
         <span class="org-string">"document.querySelector('#close-button .label').click()"</span> t)
        (sleep-for 1)))))
</pre></div></details>
<p></p>

<p>
Another example of a hacky Spookfox workaround was publishing the
unlisted videos. I couldn't figure out how to properly authenticate
with the Toobnix (Peertube) API to change the visibility of videos.
Peertube uses AngularJS components in the front end, so using
<code>.click()</code> on the input elements didn't seem to trigger anything. I
found out that I needed to use <code>.dispatchEvent(new Event('input'))</code> to
tell the dropdown for the visibility to display the options. <a href="https://stackoverflow.com/questions/67633828/angular-and-vanila-js-set-input-value">source</a>
</p>

<p>
</p><details open=""><summary>emacsconf-extract-toobnix-publish-video-from-edit-page: Messy hack to set a video to public and store the URL.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-toobnix-publish-video-from-edit-page</span> ()
  <span class="org-doc">"Messy hack to set a video to public and store the URL."</span>
  (<span class="org-keyword">interactive</span>)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('label[for=privacy]').scrollIntoView(); document.querySelector('label[for=privacy]').closest('</span><span class="org-string"><span class="org-constant">.form-group</span></span><span class="org-string">').querySelector('</span><span class="org-string"><span class="org-constant">input</span></span><span class="org-string">').dispatchEvent(new Event('</span><span class="org-string"><span class="org-constant">input</span></span><span class="org-string">'));"</span> t)
  (sit-for 1)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('span[title=\"Anyone can see this video\"]').click()"</span> t)
  (sit-for 1)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">button.orange-button</span></span><span class="org-string">').click()"</span> t)(sit-for 3)
  (emacsconf-extract-store-url)
  (shell-command <span class="org-string">"xdotool key Alt+Tab sleep 1 key Ctrl+w Alt+Tab"</span>))
</pre></div></details>
<p></p>

<p>
It's a little nicer using Spookfox to automate browser interactions
than using xdotool, since I can get data out of it too. I could also
have used Puppeteer from either Python or NodeJS, but it's nice
staying with Emacs Lisp. Spookfox has some Javascript limitations
(can't close windows, etc.), so I might still use bits of xdotool or
Puppeteer to work around that. Still, it's nice to now have an idea of
how to talk to AngularJS components.
</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Femacsconf-backstage-using-spookfox-to-publish-youtube-and-toobnix-video-drafts%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>EmacsConf backstage: Making a (play)list, checking it twice</title>
		<link>https://sachachua.com/blog/2023/12/emacsconf-backstage-making-a-play-list-checking-it-twice/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 12 Dec 2023 15:50:37 GMT</pubDate>
    <category>emacs</category>
<category>emacsconf</category>
<category>spookfox</category>
<category>youtube</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/12/emacsconf-backstage-making-a-play-list-checking-it-twice/</guid>
		<description><![CDATA[<p>
I wanted the <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUdrW3JkugtKv8xPelUoOyP">EmacsConf 2023 Youtube</a> and
<a href="https://toobnix.org/w/p/nMXCCJ25wxKUtbuQiwkakA">Toobnix playlists</a>
to mostly reflect the schedule of the conference by track, with talks
followed by their Q&amp;A sessions (if recorded).
</p>
<div id="outline-container-org8e7129c" class="outline-2">
<h3 id="org8e7129c">The list</h3>
<div class="outline-text-2" id="text-org8e7129c">
<p>
I used Emacs Lisp to generate a list of videos in the order I wanted.
That Sunday closing remarks aren't actually in the playlists because
they're combined with the Q&amp;A for my session on how we run Emacsconf.
</p>

<p>
</p><details><summary>emacsconf-extract-check-playlists: Return a table for checking playlist order.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-check-playlists</span> ()
  <span class="org-doc">"Return a table for checking playlist order."</span>
  (<span class="org-keyword">let</span> ((pos 0))
    (seq-mapcat (<span class="org-keyword">lambda</span> (o)
                  (delq
                   nil
                   (list
                    (<span class="org-keyword">when</span> (emacsconf-talk-file o <span class="org-string">"&#45;&#45;main.webm"</span>)
                      (<span class="org-keyword">cl-incf</span> pos)
                      (list pos
                            (plist-get o <span class="org-builtin">:title</span>)
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:youtube-url</span>)
                             <span class="org-string">"YouTube"</span>)
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:toobnix-url</span>)
                             <span class="org-string">"Toobnix"</span>)))
                    (<span class="org-keyword">when</span> (emacsconf-talk-file o <span class="org-string">"&#45;&#45;answers.webm"</span>)
                      (<span class="org-keyword">cl-incf</span> pos)
                      (list pos (concat <span class="org-string">"Q&amp;A: "</span> (plist-get o <span class="org-builtin">:title</span>))
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:qa-youtube-url</span>)
                             <span class="org-string">"YouTube"</span>)
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:qa-toobnix-url</span>)
                             <span class="org-string">"Toobnix"</span>))))))
                (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))
</pre></div></details>
<p></p>

<div class="bordered" id="org2e50b18">
<table>


<colgroup>
<col class="org-right">

<col class="org-left">

<col class="org-left">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-right">1</td>
<td class="org-left">An Org-Mode based text adventure game for learning the basics of Emacs, inside Emacs, written in Emacs Lisp</td>
<td class="org-left"><a href="https://youtu.be/7R0yA0R1jsk">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/2oqbPJB8Wm3QSo4HCKAyVn">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-left">Authoring and presenting university courses with Emacs and a full libre software stack</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=cklJ58i-HUY">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/mAnNW7jnPq5qhUPH2dzVQf">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">3</td>
<td class="org-left">Q&amp;A: Authoring and presenting university courses with Emacs and a full libre software stack</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=d3Q1BgLhlj0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/tjHQVNVaTa8dd9cfAXXQ3W">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">4</td>
<td class="org-left">Teaching computer and data science with literate programming tools</td>
<td class="org-left"><a href="https://youtu.be/U15zUNBz2CU">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/b4eLjcLo9vcewVTzrv95L8">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">5</td>
<td class="org-left">Q&amp;A: Teaching computer and data science with literate programming tools</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=eP4CIw_L4Mw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/fCxBagZrbR5QrgD9YcrtTJ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">6</td>
<td class="org-left">Who needs Excel? Managing your students qualifications with org-table</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=UzDqOrFGWbw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/p8K8mtayv2HYtw1gK3zUwR">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">7</td>
<td class="org-left">one.el: the static site generator for Emacs Lisp Programmers</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=AaRCuN0flRE">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/x2yYYWLHQe75FTV8sWiDmy">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">8</td>
<td class="org-left">Q&amp;A: one.el: the static site generator for Emacs Lisp Programmers</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Uq5IbkI7G7A">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/2b6Jdhxd2PBekPRCNv9c21">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">9</td>
<td class="org-left">Emacs turbo-charges my writing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=HxlEK6W7RyA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ke3UCJaJSLyQr7Emv8VxST">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">10</td>
<td class="org-left">Q&amp;A: Emacs turbo-charges my writing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=CEflOQRvLiw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/dC3RnBATHdZwgfjD9QKgZY">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">11</td>
<td class="org-left">Why Nabokov would use Org-Mode if he were writing today</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=-E_uNxwL2_I">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/bDou9TDETryMt18KcdB56A">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">12</td>
<td class="org-left">Q&amp;A: Why Nabokov would use Org-Mode if he were writing today</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=uPemMZV1r-0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/7BVMF29fRn3JAeRsuCnKnz">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">13</td>
<td class="org-left">Collaborative data processing and documenting using org-babel</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=fz7-Kd83IjM">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/7AAwoawr5MXNSrqiHJQoak">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">14</td>
<td class="org-left">How I play TTRPGs in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=KUMkj9HWiEY">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/oNkcCHdWCKXRv6KnUTAeEC">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">15</td>
<td class="org-left">Q&amp;A: How I play TTRPGs in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=bC6KWTR1Zz4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/rzufpMjdQyGnqNBKC7jKXp">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">16</td>
<td class="org-left">Org-Mode workflow: informal reference tracking</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=qx1yeJ1Exrw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/cYpEatASFWXLzDfKH4Fhec">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">17</td>
<td class="org-left">(Un)entangling projects and repos</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=o9j4IwJsvPI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/wLxyZBoFAad575Lp4PGyoF">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">18</td>
<td class="org-left">Emacs development updates</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=SPSoRZVJUf8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/57HSebb9a9JZynh2B3ehze">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">19</td>
<td class="org-left">Emacs core development: how it works</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=2izQJiuL0vA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/m4XmrmE9Geat54AKT1RQaH">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">20</td>
<td class="org-left">Top 10 ways Hyperbole amps up Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=BysjfL25Nlc">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4Cpb89zHKgQjob3gHUs73C">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">21</td>
<td class="org-left">Using Koutline for stream of thought journaling</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=dO-gv898Vmg">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/vV7qtK176DVE6RLXrZ18Ee">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">22</td>
<td class="org-left">Parallel text replacement</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=fUbBIWOJFh4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/t3G5zo35epS6HvVot9MdZv">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">23</td>
<td class="org-left">Q&amp;A: Parallel text replacement</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=lKpvJqRXu-E">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/hQtZZfYvzoUsDxSySigK33">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">24</td>
<td class="org-left">Eat and Eat powered Eshell, fast featureful terminal inside Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=KQ5Jt-63G9U">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/t4pPDtbXiZdHHEyWJVUtNs">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">25</td>
<td class="org-left">The browser in a buffer</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=mp6gaVjmKIU">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1quXfJqC9bh9VxkA9UC21x">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">26</td>
<td class="org-left">Speedcubing in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Q5HPmyaiu4g">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/2DYX2o8kB1Rv8Mqaj7H1Dx">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">27</td>
<td class="org-left">Emacs MultiMedia System (EMMS)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=kII413hkyis">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ppdF62LysvxpXgZVaeF9wk">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">28</td>
<td class="org-left">Q&amp;A: Emacs MultiMedia System (EMMS)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=KJGBASdI2JI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/b5xiqdxWCGyCo2cdsK3v9h">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">29</td>
<td class="org-left">Programming with steno</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=McHurKmk-rQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1xodScC6DPkfbnqG5FmbB3">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">30</td>
<td class="org-left">Mentoring VS-Coders as an Emacsian (or How to show not tell people about the wonders of Emacs)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=44rt1f1llhQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/sV9eKtGiPYZi5urxjoqerv">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">31</td>
<td class="org-left">Q&amp;A: Mentoring VS-Coders as an Emacsian (or How to show not tell people about the wonders of Emacs)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Bpv9JxUO4GQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/fwef53HxTKFe8ox4rz6fv8">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">32</td>
<td class="org-left">Emacs saves the Web (maybe)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=kqOZwsylo48">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/fvzGU4cQQ2meZVKNGEHMht">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">33</td>
<td class="org-left">Q&amp;A: Emacs saves the Web (maybe)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=p5dsKRWrF3s">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/9XrHVFwLeTVNQnkerSLvUt">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">34</td>
<td class="org-left">Sharing Emacs is Caring Emacs: Emacs education and why I embraced video</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=-tx72HJNfOc">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/3b5XfkceUaRjJuN5Pumgee">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">35</td>
<td class="org-left">Q&amp;A: Sharing Emacs is Caring Emacs: Emacs education and why I embraced video</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=GbFFhj0pvCE">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/w5vRTCwyMHJM3WHAgnMpBZ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">36</td>
<td class="org-left">MatplotLLM, iterative natural language data visualization in org-babel</td>
<td class="org-left"><a href="https://youtu.be/LhhFA5i_Os4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/7bwq1vAqYzY24iEMYAdcB1">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">37</td>
<td class="org-left">Enhancing productivity with voice computing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Z7l1ImjXOWM">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/vYHj7iSYhUbTxDv93NvzzY">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">38</td>
<td class="org-left">Q&amp;A: Enhancing productivity with voice computing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7E6gtxlbk3I">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/dNTjvu2RDXBckTu8ZVsWvC">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">39</td>
<td class="org-left">LLM clients in Emacs, functionality and standardization</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=HN3Y75D4tEs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ck1LWXvRiAGNLWFA8s4Ymi">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">40</td>
<td class="org-left">Q&amp;A: LLM clients in Emacs, functionality and standardization</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=jLvuR3xjoOs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/54Dvx1e93HvpyeHULcYo5Z">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">41</td>
<td class="org-left">Improving compiler diagnostics with overlays</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=g7mwN5QtcmA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/5fJkawU4R9b1dJq5BcDykx">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">42</td>
<td class="org-left">Q&amp;A: Improving compiler diagnostics with overlays</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=pjnil_gZpf8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/teTgghXfTj1cwwPhTzqfve">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">43</td>
<td class="org-left">Editor Integrated REPL Driven Development for all languages</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=1bk0pqpMCfQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/74srjNx1cgMr5MsJ9NWNNi">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">44</td>
<td class="org-left">REPLs in strange places: Lua, LaTeX, LPeg, LPegRex, TikZ</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=lGjfzfC1CH0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/oAjqkLNfo9B63EE1G6cJJV">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">45</td>
<td class="org-left">Literate Documentation with Emacs and Org Mode</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=BAFZ-vTnfSo">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/8ak16Qy1tjeFEqmcnan6MQ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">46</td>
<td class="org-left">Q&amp;A: Literate Documentation with Emacs and Org Mode</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=2g8Px71GEq8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/6VJjyqjPnCw2Pd4hVZGYvB">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">47</td>
<td class="org-left">Windows into Freedom</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7aVgVd2_HTs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4DeRkvJyKFdCBLWnHtsZW2">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">48</td>
<td class="org-left">Bringing joy to Scheme programming</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=F-H3YQywr-4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4moUfTEo2G8we5JuLGArWx">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">49</td>
<td class="org-left">Q&amp;A: Bringing joy to Scheme programming</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=GQY64ngbiF8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1t3hLzUmaNqtjE9aQXFbad">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">50</td>
<td class="org-left">GNU Emacs: A World of Possibilities</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=T5yZZK18w5w">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/jFaSuNYt2FqibtcAvmVdbF">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">51</td>
<td class="org-left">Q&amp;A: GNU Emacs: A World of Possibilities</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=bxBfbd4ezU8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/bBtwrYpKWdffVRakQCf27F">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">52</td>
<td class="org-left">A modern Emacs look-and-feel without pain</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=E1u6DcHis9M">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1DRDY8vZK3SW5M8zAPJQSp">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">53</td>
<td class="org-left">The Emacsen family, the design of an Emacs and the importance of Lisp</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7SGcLpjC5CE">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/qgJ84RLV2FZYyeSusDskwU">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">54</td>
<td class="org-left">Q&amp;A: The Emacsen family, the design of an Emacs and the importance of Lisp</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=wHhqq30bR60">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/97CWyEHugrureSjCSWHGMy">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">55</td>
<td class="org-left">emacs-gc-stats: Does garbage collection actually slow down Emacs?</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=YA1RJxH4xfQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ngenUPBLDDkZGmsxK8vimJ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">56</td>
<td class="org-left">Q&amp;A: emacs-gc-stats: Does garbage collection actually slow down Emacs?</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=X2rsy-WXUFI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/mkgAZPPGB8yLmNns3W193K">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">57</td>
<td class="org-left">hyperdrive.el: Peer-to-peer filesystem in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7tcpmZrvz9w">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/9wLA55XACiGnS3nNBNwsV5">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">58</td>
<td class="org-left">Q&amp;A: hyperdrive.el: Peer-to-peer filesystem in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=C7IikdsdXtg">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/dTxH3GjrJi4hXVz7i72G34">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">59</td>
<td class="org-left">Writing a language server in OCaml for Emacs, fun, and profit</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=VhUIS55UbQs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/jgMzmGyx4H1YDwc5n1eRZu">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">60</td>
<td class="org-left">Q&amp;A: Writing a language server in OCaml for Emacs, fun, and profit</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=L0w2_63c05A">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/umt4tcX8ufvM7xEVoe7KgC">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">61</td>
<td class="org-left">What I learned by writing test cases for GNU Hyperbole</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=maNQSKxXIzI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4XmcGSe3TQrJJNUqQXqK2B">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">62</td>
<td class="org-left">Q&amp;A: What I learned by writing test cases for GNU Hyperbole</td>
<td class="org-left"><a href="https://youtube.com/watch?v=EQFpZQoqtYI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/qBmDg6Dm5ppCEdnKVEm7ih">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">63</td>
<td class="org-left">EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=uTregv3rNl0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/eX2dXG3xMtUHuuBz4fssGT">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">64</td>
<td class="org-left">Q&amp;A: EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=nhCe5k_smZA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/18zetSzZEs6cfARaKLqiPa">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">65</td>
<td class="org-left">Saturday opening remarks</td>
<td class="org-left"><a href="https://youtu.be/piEHmLVtG6A">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/wEZX2JkDFpFqNFXnYeQTyb">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">66</td>
<td class="org-left">Saturday closing remarks</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=6OOUfBF6t7k">YouTube</a></td>
<td class="org-left"><a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUdrW3JkugtKv8xPelUoOyP">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">67</td>
<td class="org-left">Sunday opening remarks</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=B3NKI5Mviq8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/9zjMPEZz1nYokiY7rk4wYv">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">68</td>
<td class="org-left">Sunday closing remarks</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=qqvelKB5v0c">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/p/nMXCCJ25wxKUtbuQiwkakA">Toobnix</a></td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<div id="outline-container-org8a4082e" class="outline-2">
<h3 id="org8a4082e">YouTube</h3>
<div class="outline-text-2" id="text-org8a4082e">
<p>
 I bulk-added the Youtube videos to the playlist. The videos were not
in order because I uploaded some late submissions and forgotten
videos, which then got added to the end of the list.
</p>

<p>
I tried using the API to sort the playlist. This got it most of the
way there, and then I sorted the rest by hand. 
</p>

<p>
</p><details><summary>emacsconf-extract-youtube-api-sort-playlist: Try to roughly sort the playlist.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-api-sort-playlist</span> (<span class="org-type">&amp;optional</span> dry-run-only)
  <span class="org-doc">"Try to roughly sort the playlist."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-playlist (seq-find (<span class="org-keyword">lambda</span> (o) (<span class="org-keyword">let-alist</span> o (string= .snippet.title (concat emacsconf-name <span class="org-string">" "</span> emacsconf-year))))
                                        (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span> emacsconf-extract-youtube-api-playlists)))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-playlist-items
        (emacsconf-extract-youtube-api-paginated-request (concat <span class="org-string">"https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&amp;forMine=true&amp;order=date&amp;maxResults=100&amp;playlistId="</span>
                                                (url-hexify-string (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> emacsconf-extract-youtube-api-playlist)))))
  (<span class="org-keyword">let*</span> ((playlist-info emacsconf-extract-youtube-api-playlists)
         (playlist-items emacsconf-extract-youtube-api-playlist-items)
         (info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))
         (slugs (seq-map (<span class="org-keyword">lambda</span> (o) (plist-get o <span class="org-builtin">:slug</span>)) info))
         (position (1- (length playlist-items)))
         result)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">sort items</span>
    (mapc (<span class="org-keyword">lambda</span> (talk)
            (<span class="org-keyword">when</span> (plist-get talk <span class="org-builtin">:qa-youtube-id</span>)
              <span class="org-comment-delimiter">;; </span><span class="org-comment">move the q &amp; a</span>
              (<span class="org-keyword">let</span> ((video-object (emacsconf-extract-youtube-find-url-video-in-list
                                   (plist-get talk <span class="org-builtin">:qa-youtube-url</span>)
                                   playlist-items)))
                (<span class="org-keyword">let-alist</span> video-object
                  (<span class="org-keyword">cond</span>
                   ((null video-object)
                    (message <span class="org-string">"Could not find video for %s"</span> (plist-get talk <span class="org-builtin">:slug</span>)))
                   <span class="org-comment-delimiter">;; </span><span class="org-comment">not in the right position, try to move it</span>
                   ((&lt; .snippet.position position)
                    (<span class="org-keyword">let</span> ((video-id .id)
                          (playlist-id .snippet.playlistId)
                          (resource-id .snippet.resourceId))
                      (message <span class="org-string">"Trying to move %s Q&amp;A to %d from %d"</span> (plist-get talk <span class="org-builtin">:slug</span>) position .snippet.position)
                      (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">result</span> (list (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">"answers"</span> .snippet.position position))
                      (<span class="org-keyword">unless</span> dry-run-only
                        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">put</span> <span class="org-string">"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"</span>
                          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>))
                                     (<span class="org-string">"Accept"</span> . <span class="org-string">"application/json"</span>)
                                     (<span class="org-string">"Content-Type"</span> . <span class="org-string">"application/json"</span>))
                          <span class="org-builtin">:body</span> (json-encode
                                 <span class="org-highlight-quoted-quote">`</span>((id . ,video-id)
                                   (snippet
                                    (playlistId . ,playlist-id)
                                    (resourceId . ,resource-id)
                                    (position . ,position))))))))))
                (<span class="org-keyword">setq</span> position (1- position))))
            <span class="org-comment-delimiter">;; </span><span class="org-comment">move the talk if needed</span>
            (<span class="org-keyword">let</span> ((video-object
                   (emacsconf-extract-youtube-find-url-video-in-list
                    (plist-get talk <span class="org-builtin">:youtube-url</span>)
                    playlist-items)))
              (<span class="org-keyword">let-alist</span> video-object
                (<span class="org-keyword">cond</span>
                 ((null video-object)
                  (message <span class="org-string">"Could not find video for %s"</span> (plist-get talk <span class="org-builtin">:slug</span>)))
                 <span class="org-comment-delimiter">;; </span><span class="org-comment">not in the right position, try to move it</span>
                 ((&lt; .snippet.position position)
                  (<span class="org-keyword">let</span> ((video-id .id)
                        (playlist-id .snippet.playlistId)
                        (resource-id .snippet.resourceId))
                    (message <span class="org-string">"Trying to move %s to %d from %d"</span> (plist-get talk <span class="org-builtin">:slug</span>) position .snippet.position)
                    (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">result</span> (list (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">"main"</span> .snippet.position position))
                    (<span class="org-keyword">unless</span> dry-run-only
                      (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">put</span> <span class="org-string">"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"</span>
                        <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>))
                                   (<span class="org-string">"Accept"</span> . <span class="org-string">"application/json"</span>)
                                   (<span class="org-string">"Content-Type"</span> . <span class="org-string">"application/json"</span>))
                        <span class="org-builtin">:body</span> (json-encode
                               <span class="org-highlight-quoted-quote">`</span>((id . ,video-id)
                                 (snippet
                                  (playlistId . ,playlist-id)
                                  (resourceId . ,resource-id)
                                  (position . ,position))))))
                    ))))
              (<span class="org-keyword">setq</span> position (1- position))))
          (nreverse info))
    result))
</pre></div></details>
<p></p>

<p>
 I needed to sort some of the videos manually. Trying to scroll by
dragging items to the top of the currently-displayed section of the
list was slow, and dropping the item near the top of the list so that
I could pick it up again after paging up was a little disorienting.
Fortunately, keyboard scrolling with page-up and page-down worked even
while dragging an item, so that was what I ended up doing: select the
item and then page-up while dragging.
</p>

<p>
YouTube doesn't display numbers for the playlist positions, but this
will add them. The numbers don't dynamically update when the list is
reordered, so I just re-ran the code after moving things around.
</p>

<p>
</p><details><summary>emacsconf-extract-youtube-spookfox-add-playlist-numbers: Number the playlist for easier checking.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-spookfox-add-playlist-numbers</span> ()
  <span class="org-doc">"Number the playlist for easier checking.</span>
<span class="org-doc">Related: `</span><span class="org-doc"><span class="org-constant">emacsconf-extract-check-playlists</span></span><span class="org-doc">'."</span>
  (<span class="org-keyword">interactive</span>)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"[...document.querySelectorAll('</span><span class="org-string"><span class="org-constant">ytd-playlist-video-renderer</span></span><span class="org-string">')].forEach((o, i) =&gt; { o.querySelector('</span><span class="org-string"><span class="org-constant">.number</span></span><span class="org-string">')?.remove(); let div = document.createElement('</span><span class="org-string"><span class="org-constant">div</span></span><span class="org-string">'); div.classList.add('</span><span class="org-string"><span class="org-constant">number</span></span><span class="org-string">'); div.textContent = i; o.prepend(div) }))"</span> t))
</pre></div></details>
<p></p>


<figure id="org82934b4">
<img src="https://sachachua.com/blog/2023/12/emacsconf-backstage-making-a-play-list-checking-it-twice/2023-12-11_12-57-25.png" alt="2023-12-11_12-57-25.png">

<figcaption><span class="figure-number">Figure 1: </span>Adding numbers to the Youtube playlist</figcaption>
</figure>

<p>
In retrospect, I could
probably have just cleared the playlist and then added the videos using the
in the right order instead of fiddling with inserting things.
</p>
</div>
</div>
<div id="outline-container-org9a7ad62" class="outline-2">
<h3 id="org9a7ad62">Toobnix (Peertube)</h3>
<div class="outline-text-2" id="text-org9a7ad62">
<p>
Toobnix (Peertube) doesn't seem to have a way to bulk-add videos to a
playlist (or even to bulk-set their visibility). I started trying to
figure out how to use the API, but I got stuck because my token didn't
seem to let me access unlisted videos or do other things that required
proper authentication. Anyway, I came up with this messy hack to open
the videos in sequence and add them to the playlist using Spookfox.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-toobnix-set-up-playlist</span> ()
  (<span class="org-keyword">interactive</span>)
  (mapcar
   (<span class="org-keyword">lambda</span> (o)
     (<span class="org-keyword">when</span> (plist-get o <span class="org-builtin">:toobnix-url</span>)
       (browse-url (plist-get o <span class="org-builtin">:toobnix-url</span>))
       (read-key <span class="org-string">"press a key when page is loaded"</span>)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">.action-button-save</span></span><span class="org-string">').click()"</span> t)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">my-peertube-checkbox</span></span><span class="org-string">').click()"</span> t)
       (read-key <span class="org-string">"press a key when saved to playlist"</span>))
     (<span class="org-keyword">when</span> (plist-get o <span class="org-builtin">:qa-toobnix-url</span>)
       (browse-url (plist-get o <span class="org-builtin">:qa-toobnix-url</span>))
       (read-key <span class="org-string">"press a key when page is loaded"</span>)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">.action-button-save</span></span><span class="org-string">').click()"</span> t)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">my-peertube-checkbox</span></span><span class="org-string">').click()"</span> t)
       (read-key <span class="org-string">"press a key when saved to playlist"</span>)))
   (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
</pre>
</div>

<p>
Maybe next year, I might be able to figure out how to use the APIs to
do this stuff automatically.
</p>

<p>
This code is in <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-extract.el">emacsconf-extract.el</a>.
</p>
</div>
</div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Femacsconf-backstage-making-a-play-list-checking-it-twice%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item>
	</channel>
</rss>