<?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 - emacsconf</title>
	<atom:link href="https://sachachua.com/blog/category/emacsconf/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/emacsconf" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/emacsconf/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Fri, 24 Apr 2026 16:36:07 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>EmacsConf 2025 notes</title>
		<link>https://sachachua.com/blog/2026/01/emacsconf-2025-notes/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 02 Jan 2026 17:20:06 GMT</pubDate>
    <category>emacs</category>
<category>emacsconf</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/01/emacsconf-2025-notes/</guid>
		<description><![CDATA[<div class="audience" id="org18eeed8">
<p>
Intended audience: This is a long post (~ 8,400
words). It's mostly for me, but I hope it might
also be interesting if you like to use Emacs to do
complicated things or if you also happen to
organize online conferences.
</p>

</div>

<div class="update" id="org57d7df6">
<p>
<span class="timestamp-wrapper"><time class="timestamp" datetime="2026-01-02">[2026-01-02 Fri]</time></span>: Added a note about streaming from virtual desktops.
</p>

</div>

<p>
<a href="https://emacsconf.org/2025/talks/">The videos have been uploaded</a> and thank-you notes have been sent, so now I get to write quick notes on <a href="https://emacsconf.org/2025">EmacsConf 2025</a> and reuse some of my code from <a href="https://sachachua.com/blog/2024/12/emacsconf-2024-notes/">last year's post</a>.
</p>


<div class="sticky-toc" id="org7a025d4">
<div class="text-table-of-contents toc-id" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-stats">Stats</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-timeline">Timeline</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-managing-conference-information-in-org">Managing conference information in Org Mode and Emacs</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-communication">Communication</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-schedule">Schedule</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-recorded-introductions">Recorded introductions</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-recorded-videos">Recorded videos</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-captioning">Captioning</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-bigbluebutton-web-conference">BigBlueButton web conference</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-checking-speakers-in">Checking speakers in</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-hosting">Hosting</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-infrastructure">Infrastructure</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-streaming">Streaming</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-publishing">Publishing</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-etherpad">Etherpad</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-irc">IRC</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-extracting-the-q-a">Extracting the Q&amp;A</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-budget">Budget</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-documentation-and-time">Tracking my time</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-thanks">Thanks</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2025-notes-overall">Overall</a></li>
</ul>
</div>

</div>
<div id="outline-container-emacsconf-2025-notes-stats" class="outline-3">
<h3 id="emacsconf-2025-notes-stats">Stats</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-stats">
<p>
While organizing EmacsConf 2025, I thought it was
going to be a smaller conference compared to last
year because of lots of last-minute cancellations.
Now that I can finally add things up, I see how it
all worked out:
</p>

<table>


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

<col class="org-right">

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">2024</th>
<th scope="col" class="org-right">2025</th>
<th scope="col" class="org-left">Type</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">31</td>
<td class="org-right">25</td>
<td class="org-left">Presentations</td>
</tr>

<tr>
<td class="org-right">10.7</td>
<td class="org-right">11.3</td>
<td class="org-left">Presentation duration (hours)</td>
</tr>

<tr>
<td class="org-right">21</td>
<td class="org-right">11</td>
<td class="org-left">Q&amp;A web conferences</td>
</tr>

<tr>
<td class="org-right">7.8</td>
<td class="org-right">5.2</td>
<td class="org-left">Q&amp;A duration (hours)</td>
</tr>

<tr>
<td class="org-right">18.5</td>
<td class="org-right">16.5</td>
<td class="org-left">Total</td>
</tr>
</tbody>
</table>

<p>
EmacsConf 2025 was actually a little longer than 2024 in total presentation time, although that's probably because we had more live talks which included answering questions.
It was almost as long overall including live Q&amp;A in BigBlueButton rooms, but we did end an hour or so earlier each day.
</p>

<p>
Looking at the livestreaming data, I see that we had fewer participants compared to the previous year. Here are the stats from Icecast, the program we use for streaming:
</p>

<ul class="org-ul">
<li>Saturday:
<ul class="org-ul">
<li>gen: 107 peak + 7 lowres (compared to 191 in 2024)</li>
<li>dev: 97 peak + 7 lowres (compared to 305 in 2024)</li>
</ul></li>
<li>Sunday: I forgot to copy Sunday stats, whoops! I think there were about 70 people on the general stream. <mark>Idea:</mark> Automate this next time.</li>
</ul>

<p>
The YouTube livestream also had fewer participants at the time of the stream, but that's okay. Here are the stats from YouTube:
</p>

<table>


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

<col class="org-right">

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">2024 peak</th>
<th scope="col" class="org-right">2025 peak</th>
<th scope="col" class="org-left">YouTube livestream</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">46</td>
<td class="org-right">23</td>
<td class="org-left"><a href="https://youtube.com/live/r41CGbNo9Sw">Gen Sat AM</a></td>
</tr>

<tr>
<td class="org-right">24</td>
<td class="org-right">7</td>
<td class="org-left"><a href="https://youtube.com/live/Y3PjMp2mBQM">Gen Sat PM</a></td>
</tr>

<tr>
<td class="org-right">15</td>
<td class="org-right">8</td>
<td class="org-left"><a href="https://youtube.com/live/KCZthyBhHtg">Dev Sat AM</a></td>
</tr>

<tr>
<td class="org-right">20</td>
<td class="org-right">14</td>
<td class="org-left"><a href="https://youtube.com/live/r41CGbNo9Sw">Dev Sat PM</a></td>
</tr>

<tr>
<td class="org-right">28</td>
<td class="org-right">14</td>
<td class="org-left"><a href="https://youtube.com/live/tzPmQ40hwis">Gen Sun AM</a></td>
</tr>

<tr>
<td class="org-right">26</td>
<td class="org-right">11</td>
<td class="org-left"><a href="https://youtube.com/live/uCNWK0Bqo9Q">Gen Sun PM</a></td>
</tr>
</tbody>
</table>

<p>
Fewer people attended compared to last year, but it's still an amazing get-together from the perspective of being able to get a bunch of Emacs geeks in a (virtual) room. People asked a lot of questions over IRC and the Etherpads, and the speakers shared lots of extra details that we captured in the Q&amp;A sessions. I'm so glad people were able to connect with each other.
</p>

<p>
Based on the e-mails I got from speakers about their presentations and the regrets from people who couldn't make it to EmacsConf, it seemed that people were a lot busier in 2025 compared to 2024. There were also a lot more stressors in people's lives. But it was still a good get-together, and it'll continue to be useful going forward.
</p>

<p>
At the moment, the <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCVGgvM1ev1Oh2vSZbFPGl3U">EmacsConf 2025 videos</a> have about 20k views total on YouTube. (media.emacsconf.org doesn't do any tracking.) Here are the most popular ones so far:
</p>

<ul class="org-ul">
<li><a href="https://www.youtube.com/watch?v=wE8vCWyr1Eo">EmacsConf 2025: Zettelkasten for regular Emacs hackers - Christian Tietze (he)</a>                    (2.8k views)</li>
<li><a href="https://www.youtube.com/watch?v=U3kbEabBJ_s">EmacsConf 2025: Emacs, editors, and LLM driven workflows - Andrew Hyatt (he/him)</a>                  (1.9k)</li>
<li><a href="https://www.youtube.com/watch?v=RVoGcLNalJw">EmacsConf 2025: Modern Emacs/Elisp hardware/software accelerated graphics - Emanuel Berg (he/him)</a> (1.2k)</li>
<li><a href="https://www.youtube.com/watch?v=KQBXTSg_Occ">EmacsConf 2025: An introduction to the Emacs Reader - Divyá</a>                                       (835)</li>
<li><a href="https://www.youtube.com/watch?v=uACM4a5MPQM">EmacsConf 2025: Interactive Python programming in Emacs - David Vujic (he/him)</a>                    (731)</li>
</ul>

<p>
While I was looking at the viewing stats on YouTube, I noticed that people are still looking at videos all the way back to <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUGJY-knn_6Ss_lcJ58IlKx">2013</a> (like <a href="https://www.youtube.com/watch?v=j4bcE_gBJrw">Emacs Live - Sam Aaron</a> and <a href="https://www.youtube.com/watch?v=QFClYrhV1z4">Emacs Lisp Development - John Wiegley</a>), and many videos have thousands of views. Here are some of the most popular ones from past conferences:
</p>

<ul class="org-ul">
<li><a href="https://www.youtube.com/watch?v=vEpk2ZTqJu4">EmacsConf 2022: What I'd like to see in Emacs - Richard M. Stallman</a>                             (54k views)</li>
<li><a href="https://www.youtube.com/watch?v=KYcY7CcS7nc">EmacsConf 2019 - 26a - Emacs: The Editor for the Next Forty Years - Perry E. Metzger (pmetzger)</a> (21k)</li>
<li><a href="https://www.youtube.com/watch?v=pzIaGD6GjjE">EmacsConf 2019 - 32 - VSCode is Better than Emacs - Making Emacs More Approachable - Zaiste</a>     (19k)</li>
<li><a href="https://www.youtube.com/watch?v=Bbjxn9yVNJ8">EmacsConf 2019 - 19 - Awesome Java editing environment - Torstein Krause Johansen</a>               (15k)</li>
<li><a href="https://www.youtube.com/watch?v=9xLeqwl_7n0">EmacsConf 2022: Top 10 reasons why you should be using Eshell - Howard Abrams (he/him)</a>          (15k)</li>
</ul>

<p>
Views aren't everything, of course, but maybe they let us imagine a little about how many people these speakers might have been able to reach. How wonderful it is that people can spend a few days putting together their insights and then have that resonate with other people through time. Speakers are giving us long-term gifts.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-timeline" class="outline-3">
<h3 id="emacsconf-2025-notes-timeline">Timeline</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-timeline">
<p>
Of course, the process of preparing all of that started long before the days of the conference. We posted the call for proposals towards the end of June, like we usually do, because we wanted to give people plenty of time to start thinking about their presentations. We did early acceptances again this year, and we basically accepted everything, so people could start working on their videos almost right away. Here's the general timeline:
</p>

<table>


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

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left">CFP</td>
<td class="org-left">2025-06-27 Fri</td>
</tr>

<tr>
<td class="org-left">CFP deadline</td>
<td class="org-left">2025-09-19 Fri</td>
</tr>

<tr>
<td class="org-left">Speaker notifications</td>
<td class="org-left">2025-09-26 Fri</td>
</tr>

<tr>
<td class="org-left">Publish schedule</td>
<td class="org-left">2025-10-24 Fri (oops, forgot to do this elsewhere)</td>
</tr>

<tr>
<td class="org-left">Video submission target date</td>
<td class="org-left">2025-10-31 Fri</td>
</tr>

<tr>
<td class="org-left">EmacsConf</td>
<td class="org-left">2025-12-06 Sat - 2025-12-07 Sun</td>
</tr>

<tr>
<td class="org-left">Live talks, Q&amp;A videos posted</td>
<td class="org-left">2025-12-17</td>
</tr>

<tr>
<td class="org-left">Q&amp;A notes posted</td>
<td class="org-left">2025-12-28</td>
</tr>

<tr>
<td class="org-left">These notes</td>
<td class="org-left">2026-01-01</td>
</tr>
</tbody>
</table>


<figure id="org610eadb">
<img src="https://sachachua.com/blog/2026/01/emacsconf-2025-notes/submissions_plot.png" alt="submissions_plot.png">

<figcaption><span class="figure-number">Figure 1: </span>EmacsConf 2025: cumulative proposals and video uploads</figcaption>
</figure>

<p>
This graph shows that we got more proposals earlier this year (solid blue line: 2025) compared to last year (gray: 2024), although there were fewer last-minute ones and more cancellations this year. Some people were very organized. (Video submissions in August!) Some people sent theirs in later because they first had to figure out all the details of what they proposed, which is totally relatable for anyone who's found themselves <a href="https://en.wiktionary.org/wiki/yak_shaving">shaving Emacs yaks</a> before.
</p>

<p>
I really appreciated the code that I wrote to <a href="https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/">create SVG previews of the schedule</a>. That made it much easier to see how the schedule changed as I added or removed talks. I started stressing out about the gap between the proposals and the uploaded videos (orange) in November. Compared to last year, the submissions slowly trickled in. The size of the gap between the blue line (cumulative proposals) and the orange line (cumulative videos uploaded) was much like my stress level, because I was wondering how I'd rearrange things if most of the talks had to cancel at the last minute or if we were dealing with a mostly-live schedule. Balancing EmacsConf anxiety with family challenges resulted in all sorts of <a href="https://sachachua.com/blog/2025/11/tracking-my-oopses/">oopses in my personal life.</a> (I even accidentally shrank my daughter's favourite T-shirt.) It helped to remind myself that we started off as a single-track single-day conference with mostly live sessions, that it's totally all right to go back to that, and that we have a wonderful (and very patient) community. I think speakers were getting pretty stressed too. I reassured speakers that Oct 31 was a soft target date, not a hard deadline. I didn't want to cancel talks just because life got busy for people.
</p>

<p>
It worked out reasonably well. We had enough capacity to process and caption the videos as they came in, even the last-minute uploads. Many speakers did their own captions. We ended up having five live talks. Live talks are generally a bit more stressful for me because I worry about technical issues or other things getting in the way, but all those talks went well, thank goodness.
</p>

<p>
For some reason, Linode doesn't seem to show me detailed accrued charges any more. I think it used to show them before, which is why I managed to write <a href="https://sachachua.com/blog/2024/12/emacsconf-2024-notes/">last year's notes</a> in December. If I skip the budget section of these notes, maybe I can post them earlier, and then just follow up once the invoices are out.
</p>

<p>
I think the timeline worked out mostly all right this year. I don't think moving the target date for videos earlier would have made much of a difference, since speakers would probably still be influenced by the actual date of the conference. It's hard to stave off that feeling of pre-conference panic: Do we have enough speakers? Do we have enough videos? But these graphs might help me remember that it's been like that <a href="https://sachachua.com/blog/2023/10/emacsconf-backstage-looking-at-emacsconf-s-growth-over-5-years-and-how-to-do-pivot-tables-and-graphs-with-org-mode-and-the-python-pandas-library/">before</a> and it has still worked out, so it might just be part of my job as an organizer to embrace the uncertainty. Most conferences do live talks, anyway.
</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>Emacs Lisp code for making the table</strong></summary>
<p>

</p><div class="org-src-container">
<pre class="src src-emacs-lisp" id="org30310d6"><code>(append <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"slug"</span> <span class="org-string">"submitted"</span> <span class="org-string">"uploaded"</span> <span class="org-string">"cancelled"</span>) hline)
        (sort (delq nil
                    (org-map-entries
                     (<span class="org-keyword">lambda</span> ()
                       (list
                        (org-entry-get (point) <span class="org-string">"SLUG"</span>)
                        (org-entry-get (point) <span class="org-string">"DATE_SUBMITTED"</span>)
                        (org-entry-get (point) <span class="org-string">"DATE_UPLOADED"</span>)
                        (org-entry-get (point) <span class="org-string">"DATE_CANCELLED"</span>)))
                     <span class="org-string">"DATE_SUBMITTED={.}"</span>))
              <span class="org-builtin">:key</span> (<span class="org-keyword">lambda</span> (o) (format <span class="org-string">"%s - %s"</span> (elt o 3) (elt o 2)))
              <span class="org-builtin">:lessp</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string&lt;</span>) <span class="org-warning">nil)</span>
</code></pre>
</div>


<p></p>


</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>Python code for plotting the graph</strong></summary>

<div class="org-src-container">
<pre class="src src-python"><code><span class="org-keyword">import</span> pandas <span class="org-keyword">as</span> pd
<span class="org-keyword">import</span> seaborn <span class="org-keyword">as</span> sns
<span class="org-keyword">import</span> matplotlib.pyplot <span class="org-keyword">as</span> plt
<span class="org-keyword">import</span> matplotlib.dates <span class="org-keyword">as</span> mdates
<span class="org-keyword">import</span> io

<span class="org-variable-name">all_dates</span> <span class="org-operator">=</span> pd.date_range(start<span class="org-operator">=</span><span class="org-string">'2025-06-01'</span>, end<span class="org-operator">=</span><span class="org-string">'2025-12-09'</span>, freq<span class="org-operator">=</span><span class="org-string">'D'</span>)
<span class="org-keyword">def</span> <span class="org-function-name">convert</span>(rows):
    <span class="org-variable-name">df</span> <span class="org-operator">=</span> pd.DataFrame(rows, columns<span class="org-operator">=</span>[<span class="org-string">'slug'</span>, <span class="org-string">'proposal_date'</span>, <span class="org-string">'upload_date'</span>, <span class="org-string">'cancelled_date'</span>])
    <span class="org-variable-name">df</span> <span class="org-operator">=</span> df.<span class="org-builtin">map</span>(<span class="org-keyword">lambda</span> x: pd.NA <span class="org-keyword">if</span> (x <span class="org-operator">==</span> [] <span class="org-keyword">or</span> x <span class="org-operator">==</span> <span class="org-string">"nil"</span>) <span class="org-keyword">else</span> x)
    <span class="org-variable-name">df</span>[<span class="org-string">'proposal_date'</span>] <span class="org-operator">=</span> pd.to_datetime(df[<span class="org-string">'proposal_date'</span>]).apply(<span class="org-keyword">lambda</span> d: d.replace(year<span class="org-operator">=</span>2025))
    <span class="org-variable-name">df</span>[<span class="org-string">'upload_date'</span>] <span class="org-operator">=</span> pd.to_datetime(df[<span class="org-string">'upload_date'</span>]).apply(<span class="org-keyword">lambda</span> d: d.replace(year<span class="org-operator">=</span>2025))
    <span class="org-variable-name">df</span>[<span class="org-string">'cancelled_date'</span>] <span class="org-operator">=</span> pd.to_datetime(df[<span class="org-string">'cancelled_date'</span>]).apply(<span class="org-keyword">lambda</span> d: d.replace(year<span class="org-operator">=</span>2025))
    <span class="org-variable-name">prop_counts</span> <span class="org-operator">=</span> df.groupby(<span class="org-string">'proposal_date'</span>).size().reindex(all_dates, fill_value<span class="org-operator">=</span>0).cumsum()
    <span class="org-variable-name">additions</span> <span class="org-operator">=</span> df[df[<span class="org-string">'proposal_date'</span>].notna()].groupby(<span class="org-string">'proposal_date'</span>).size().reindex(all_dates, fill_value<span class="org-operator">=</span>0)
    <span class="org-variable-name">subtractions</span> <span class="org-operator">=</span> df[df[<span class="org-string">'cancelled_date'</span>].notna()].groupby(<span class="org-string">'cancelled_date'</span>).size().reindex(all_dates, fill_value<span class="org-operator">=</span>0)
    <span class="org-variable-name">upload_counts</span> <span class="org-operator">=</span> df[df[<span class="org-string">'upload_date'</span>].notna()].groupby(<span class="org-string">'upload_date'</span>).size().reindex(all_dates, fill_value<span class="org-operator">=</span>0).cumsum()
    <span class="org-variable-name">prop_counts</span> <span class="org-operator">=</span> (additions <span class="org-operator">-</span> subtractions).cumsum()
    <span class="org-keyword">return</span> prop_counts, upload_counts

<span class="org-variable-name">prop_counts</span>, <span class="org-variable-name">upload_counts</span> <span class="org-operator">=</span> convert(current_data)
<span class="org-variable-name">prop_counts_2024</span>, <span class="org-variable-name">upload_counts_2024</span> <span class="org-operator">=</span> convert(previous_data)

<span class="org-variable-name">plot_df</span> <span class="org-operator">=</span> pd.DataFrame({
    <span class="org-string">'Date'</span>: all_dates,
    <span class="org-string">'2025 Proposals'</span>: prop_counts.values,
    <span class="org-string">'2025 Videos'</span>: upload_counts.values,
})
<span class="org-variable-name">plot_df_melted</span> <span class="org-operator">=</span> plot_df.melt(id_vars<span class="org-operator">=</span><span class="org-string">'Date'</span>, var_name<span class="org-operator">=</span><span class="org-string">'Type'</span>, value_name<span class="org-operator">=</span><span class="org-string">'Count'</span>)
sns.set_theme(style<span class="org-operator">=</span><span class="org-string">"whitegrid"</span>)
plt.figure(figsize<span class="org-operator">=</span>(12, 6))
plt.plot(all_dates, prop_counts_2024, color<span class="org-operator">=</span><span class="org-string">'gray'</span>, label<span class="org-operator">=</span><span class="org-string">'2024 cumulative proposals'</span>)
plt.plot(all_dates, upload_counts_2024, color<span class="org-operator">=</span><span class="org-string">'gray'</span>, alpha<span class="org-operator">=</span>0.5, label<span class="org-operator">=</span><span class="org-string">'2024 cumulative uploads'</span>)
<span class="org-variable-name">ax</span> <span class="org-operator">=</span> sns.lineplot(data<span class="org-operator">=</span>plot_df_melted, x<span class="org-operator">=</span><span class="org-string">'Date'</span>, y<span class="org-operator">=</span><span class="org-string">'Count'</span>, hue<span class="org-operator">=</span><span class="org-string">'Type'</span>, linewidth<span class="org-operator">=</span>2.5)
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter(<span class="org-string">'%b'</span>))
plt.title(<span class="org-string">'EmacsConf 2025: cumulative proposals and video uploads'</span>, fontsize<span class="org-operator">=</span>16)
plt.xlabel(<span class="org-string">'Date'</span>, fontsize<span class="org-operator">=</span>12)
plt.ylabel(<span class="org-string">'Cumulative Count'</span>, fontsize<span class="org-operator">=</span>12)
plt.tight_layout()
plt.savefig(<span class="org-string">'submissions_plot.png'</span>)
</code></pre>
</div>



</details>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-managing-conference-information-in-org" class="outline-3">
<h3 id="emacsconf-2025-notes-managing-conference-information-in-org">Managing conference information in Org Mode and Emacs</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-managing-conference-information-in-org">
<p>
Organizing the conference meant keeping track of lots of e-mails and lots of tasks over a long period of time. To handle all of the moving parts, I relied on Org
Mode in Emacs to manage all the conference-related information.
This year, I switched to mainly using the <a href="https://emacsconf.org/organizers-notebook/">general
organizers notebook</a>, with the <a href="https://emacsconf.org/2025/organizers-notebook/">year-specific one</a>
mostly just used for the draft schedule.
I added some shortcuts to jump to headings in the
main notebook or in the annual notebook (<code>emacsconf-main-org-notebook-heading</code> and <code>emacsconf-current-org-notebook-heading</code> <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf.el">emacsconf.el</a>).
</p>

<p>
As usual, we used lots of structured entry properties to
store all the talk information.
Most of my functions from last year worked out
fine, aside from the occasional odd bug when I
forgot what property I was supposed to use. (<code>:ROOM:</code>, not <code>:BBB:</code>&hellip;)
</p>

<p>
The validation functions I thought about last year
would have been nice to have, since I ran into a
minor case-sensitivity issue with the Q&amp;A value
for one of the talks (live versus Live).
This happened last year too and it should have
been caught by the <code>case-fold-search</code> I thought I
added to my configuration, but since that didn't
seem to kick in, I should probably also deal with
it on the input side.
<mark>Idea:</mark> I want to validate that certain fields
like <code>QA_TYPE</code> have only a specified set of
values.
I also probably need a validation function to make
sure that newly-added talks have all the files
that the streaming setup expects, like an overlay
for OBS, and one that checks if I've set any
properties outside the expected list.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-communication" class="outline-3">
<h3 id="emacsconf-2025-notes-communication">Communication</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-communication">
<p>
<a href="https://lists.gnu.org/archive/html/emacsconf-org/2025-06/msg00000.html">After some revision</a>, the call for participation went out on <a href="https://lists.gnu.org/archive/html/emacs-tangents/2025-06/msg00000.html">emacs-tangents</a>, <a href="https://sachachua.com/blog/2025/06/2025-06-30-emacs-news/">Emacs News</a>, <a href="https://lists.gnu.org/archive/html/emacsconf-discuss/2025-06/msg00000.html">emacsconf-discuss</a>, <a href="https://lists.gnu.org/archive/html/emacsconf-org/2025-06/msg00005.html">emacsconf-org</a>, <a href="https://www.reddit.com/r/emacs/comments/1lmpw8s/call_for_proposals_open_for_emacsconf_2025/">Reddit</a>, <a href="https://lobste.rs/s/ptsrso/emacsconf_2025_call_for_participation">lobste.rs</a>, and <a href="https://systemcrafters.net/live-streams/august-22-2025/">System Crafters</a>. (Thanks!) People also mentioned it in various meetups, and there were other threads leading up to it (<a href="https://www.reddit.com/r/emacs/comments/1otxhxy/emacsconf_will_be_in_less_than_month/">Reddit</a>, <a href="https://news.ycombinator.com/item?id=46127143">HN</a>, <a href="https://lemmy.world/post/39818970">lemmy.world</a>, <a href="https://x.com/fsf/status/1972889158558372262">@fsf</a>)
</p>

<ul class="org-ul">
<li><a href="https://corwin.bru.st/2025-09-18-emacsconf-cfp-ending-and-a-completing-read-example/">EmacsConf CFP ending and a completing-read example</a></li>
<li><a href="https://www.fsf.org/events/event-20191102-virtual-emacsconf">Event - EmacsConf — Free Software Foundation — Working together for free software</a></li>
<li><a href="https://screwlisp.small-web.org/complex/my-eepitch-send-actions-and-the-situation-calculus/">My eepitch-send, actions and the situation calculus</a></li>
<li><a href="https://debianforum.de/forum/viewtopic.php?t=193522">EmacsConf/ 2025 Online Conference am Wochenende - dem 6. und 7. Dez. 25 - debianforum.de</a></li>
</ul>

<p>
Once the speakers confirmed the tentative schedules worked for them, I published the schedule on <a href="https://emacsconf.org/2025/talks">emacsconf.org</a> on Oct 24, as sceduled. But I forgot to announce the schedule on emacsconf-discuss and other places, though. I probably felt a little uncertain about announcing the schedule because it was in such flux. There was even a point where we almost cancelled the whole thing. I also got too busy to reach out to podcasts. I remembered to post it to <a href="https://foss.events/2025/12-06-emacsconf.html">foss.events</a>, though! <mark>Idea:</mark> I can announce things and trust that it'll all settle down. I can spend some time neatening up our general <a href="https://emacsconf.org/organizers-notebook/">organizers-notebook</a> so that there's more of a step-by-step-process. Maybe <code>org-clone-subtree-with-time-shift</code> will be handy.
</p>

<p>
There were also a few posts and threads afterwards:
</p>

<ul class="org-ul">
<li><a href="https://forum.zettelkasten.de/discussion/3369/zettelkasten-for-regular-emacs-hackers-emacsconf-2025-talk">Zettelkasten for Regular Emacs Hackers &ndash; EmacsConf 2025 talk — Zettelkasten Forum</a></li>
<li><a href="http://angg.twu.net/emacsconf2025.html">Some problems of modernizing Emacs (eev @ EmacsConf 2025)</a></li>
<li><a href="https://www.linkedin.com/posts/davidvujic_emacsconf-2025-interactive-python-programming-activity-7408610909666848768-v7dV">EmacsConf 2025: Interactive Python programming in Emacs - David Vujic (he/him) | David Vujic</a></li>
<li><a href="https://news.ycombinator.com/item?id=46197513">HN: My favorite talks from emacsconf 2025</a></li>
<li><a href="https://kyo.iroiro.party/en/posts/juicemacs-exploring-jit-for-elisp/">Exploring Speculative JIT Compilation for Emacs Lisp with Java | Kyou is kyou is kyou is kyou</a></li>
</ul>

<p>
For communicating with speakers and volunteers, I reused the <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-mail.el">mail merge</a> from previous years, with a few more tweaks. I added a template for thanking volunteers.
I added some more code for mailing all the volunteers with a specific tag.
Some speakers were very used to the process and did everything pretty independently.
Other speakers needed a bit more facilitation.
</p>

<p>
I could get better at coordinating with people who want to help. In the early days, there wasn't that much to help with. A few people volunteered to help with captions for specific videos, so I waited for their edits and focused on other tasks first. I didn't want to pressure them with deadlines or preempt them by working on those when I'd gotten through my other tasks, but it was hard to leave those tasks dangling. They ended up sending in the edits shortly before the conference, which still gave me enough time to coordinate with speakers to review the captions. It was hard to wait, but I appreciate the work they contributed. In late November and early December, there were so many tasks to juggle, but I probably could have e-mailed people once a week with a summary of the things that could be done. <mark>Idea:</mark> I can practice letting people know what I'm working on and what tasks remain, and I can find a way to ask for help that fits us. If I clean up the design of the backstage area, that might make it easier for people to get started. If I improve my code for comparing captions, I can continue editing subtitles as a backup if I want to take a break from my other work, and merge in other people's contributions when they send them.
</p>

<p>
I set up <a href="https://www.mumble.info/">Mumble</a> for backstage coordination with other organizers, but we didn't use it much because most of the time, we were on air with someone, so we didn't want to interrupt each other.
</p>

<p>
There was a question about our guidelines for conduct and someone's out-of-conference postings. I forwarded the matter to our private mailing list so that other volunteers could think about it, because at that point in time, my brain was fried. That seemed to have been resolved. Too bad there's no <code>edebug-on-entry</code> for people so we can figure things out with more clarity. I'm glad other people are around to help navigate situations!
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-schedule" class="outline-3">
<h3 id="emacsconf-2025-notes-schedule">Schedule</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-schedule">
<p>
Like last year, we had two days of talks, with two tracks on the first day, and about 15-20 minutes between each talk.
We had a couple of last-minute switches to live Q&amp;A sessions, which was totally fine.
That's why I set up all the rooms in advance.
</p>

<p>
A few talks ended up being a bit longer than their
proposed length.
With enough of a heads-up, I could adjust the
schedule (especially as other talks got
cancelled), but sometimes it was difficult to keep
track of changes as I shuffled talks around.
I wrote some code to calculate the differences, and
I appreciate how the speakers patiently dealt with
my flurries of e-mails.
</p>

<p>
Scheduling some more buffer
time between talks might be good. In general, the
Q&amp;A time felt like a good length, though, and it
was nice that people had the option of continuing
with the speaker in the web conference room. So it
was actually more like we had two or three or four
tracks going on at the same time.
</p>

<p>
Having two tracks allowed us to accept all the
talks. I'm glad I kept the dev track to a single
day, though. I ended up managing things all by
myself on Sunday afternoon, and that was hard
enough with one track. Fortunately, speakers were
comfortable navigating the Etherpad questions themselves and reading the questions out loud. Sometimes I stepped in during natural pauses to read the next question on the list
(assuming I managed to find the right window among all
the ones open on my screen).
</p>

<p>
<mark>Idea:</mark> If I get better at setting up one window per ongoing Q&amp;A session and using the volume controls to spatialize each so that I can distinguish left/middle/right conversations, it might be easier for me to keep track of all of those. I wasn't quite sure how to arrange all the windows I wanted to pay attention even with an external screen (1920x1200 + my laptop's 1920x1080). I wonder if I'd trust <a href="https://github.com/emacs-exwm/exwm">EXWM</a> to handle tiling all the different web browser windows, while keeping the VNC windows floating so that they don't mess with the size of the stream. Maybe I can borrow my husband's ultrawide monitor. Maybe I can see if I can use one of those fancy macropads for quick shortcuts, like switching to a specified set of windows and unmuting myself. Maybe I can figure out how to funnel streaming captions into IRC channels or another text form (also a requested feature) so that I can monitor them that way and quickly skim the history&hellip; Or I can focus on making it easier for people to volunteer (and reminding myself that it's okay to ask for their help!), since then I don't have to split my attention all those different ways and I get to learn from other people's awesomeness.
</p>

<p>
If we decide to go with one track next year, we
might have to prioritize talks, which is hard
especially if some of the accepted speakers end up
cancelling anyway. Alternatively, a better way
might be to make things easier for last-minute
volunteers to join and read the questions. They
don't even need any special setup or anything;
they can just join the BigBlueButton web conference session and read from
the pad. <mark>Idea:</mark> A single Etherpad with all the
direct BBB URLs and pad links will make this
easier for last-minute volunteers.
</p>

<p>
Also like last year, we added an open mic session to fill in the time from a last-minute cancellation, and that went well. It might be nice to build that into the schedule earlier instead of waiting for a cancellation so that people can plan for it. We moved the closing remarks earlier as well so that people didn't have to stay up so late in other timezones.
</p>

<p>
I am super, super thankful we had a <a href="https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/">crontab</a> automatically switching between talks, because that meant one less time-sensitive thing I had to pay attention to. I didn't worry about cutting people off too early because people could continue off-stream, although this generally worked better for Q&amp;A sessions done over BigBlueButton or on Etherpad rather than IRC. I also improved the code for generating a test schedule so that I could test the automatic switching.
</p>

<p>
It was probably a good thing that I didn't automatically write the next session time to the Etherpad, though, because people had already started adding notes and questions to the pad before the conference, so I couldn't automatically update them as I changed the schedule. <mark>Idea:</mark> I can write a function to copy the heads-up for the next talk, or I can add the text to a checklist so that I can easily copy and paste it. Since I was managing the check-ins for both streams as well as doing the occasional bit of hosting, a checklist combining all the info for all the streams might be nice. I didn't really take advantage of the editability of Etherpad, so maybe putting it into HTML instead will allow me to make it easier to skim or use. (Copy icons, etc.)
</p>

<p>
Like before, I offset the start of the dev track to give ourselves a little more time to warm up, and I started Sunday morning with more asynchronous Q&amp;A instead of web conferences. Not much in terms of bandwidth issues this year.
</p>

<p>
We got everyone's time constraints correctly this year, hooray!
Doing timezone conversions in Emacs means I don't have to calculate things myself, and the code for checking the time constraints of scheduled sessions worked with this year's shifting schedules too.
<mark>Idea:</mark> It would be great to mail iCalendar files (.ics) to each speaker so that they can easily add their talk (including check-in time, URLs, and mod codes) to their calendar.
Bonus points if we can get it to update previous copies if I've made a change.
</p>

<p>
This is what the schedule looked like:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">with-temp-file</span> (expand-file-name <span class="org-string">"schedule.svg"</span> emacsconf-cache-dir)
  (<span class="org-keyword">let</span> ((emacsconf-use-absolute-url t))
    (svg-print
     (emacsconf-schedule-svg
      800 300
      (emacsconf-publish-prepare-for-display
       (emacsconf-get-talk-info))))))
</code></pre>
</div>


<p>
I added a <a href="https://emacsconf.org/2025/organizers-notebook/#draft-schedule">vertical view</a> to the 2025 organizers notebook, which was easier to read. I also added some code to handle cancelling a talk and keeping track of rescheduled talks.
</p>


<figure id="org2820bd6">
<svg width="800" height="300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>Graphical view of the schedule</title><g transform="translate(0,0)"><title>Schedule for Saturday</title><rect width="800" height="150" x="0" y="0" fill="white"></rect><text font-size="10" fill="black" y="12" x="3">Saturday</text><a href="https://emacsconf.org/2025/talks/sat-open" title="Saturday opening remarks" data-slug="sat-open"><title> 9:00- 9:10 Saturday opening remarks</title><rect x="0" y="15" opacity="0.8" width="15" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(13,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">sat-open</text></g></a><a href="https://emacsconf.org/2025/talks/org-babel" title="Making Org-Babel reactive" data-slug="org-babel"><title> 9:10- 9:20 Making Org-Babel reactive</title><rect x="15" y="15" opacity="0.8" width="15" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(28,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">org-babel</text></g></a><a href="https://emacsconf.org/2025/talks/reference" title="Emacs as a fully-fledged reference manager" data-slug="reference"><title> 9:30- 9:55 Emacs as a fully-fledged reference manager</title><rect x="47" y="15" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(84,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">reference</text></g></a><a href="https://emacsconf.org/2025/talks/gmail" title="org-gmail: A deep integration of Gmail into your Org Mode" data-slug="gmail"><title>10:15-10:40 org-gmail: A deep integration of Gmail into your Org Mode</title><rect x="117" y="15" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(154,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">gmail</text></g></a><a href="https://emacsconf.org/2025/talks/gnus" title="Reading and writing emails in GNU Emacs with Gnus" data-slug="gnus"><title>10:50-11:15 Reading and writing emails in GNU Emacs with Gnus</title><rect x="172" y="15" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(209,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">gnus</text></g></a><a href="https://emacsconf.org/2025/talks/latex" title="LaTeX export in org-mode: the overhaul" data-slug="latex"><title>11:25-11:45 LaTeX export in org-mode: the overhaul</title><rect x="227" y="15" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(256,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">latex</text></g></a><a href="https://emacsconf.org/2025/talks/calc" title="Basic Calc functionality for engineering or electronics" data-slug="calc"><title> 1:00- 1:25 Basic Calc functionality for engineering or electronics</title><rect x="376" y="15" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(413,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">calc</text></g></a><a href="https://emacsconf.org/2025/talks/blee-lcnt" title="Blee-LCNT: An Emacs-centered content production and self-publication framework" data-slug="blee-lcnt"><title> 1:35- 2:15 Blee-LCNT: An Emacs-centered content production and self-publication framework</title><rect x="431" y="15" opacity="0.8" width="62" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(491,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">blee-lcnt</text></g></a><a href="https://emacsconf.org/2025/talks/greader" title="GNU Emacs Greader (Gnamù Reader) mode is the best Emacs mode in existence" data-slug="greader"><title> 2:35- 2:40 GNU Emacs Greader (Gnamù Reader) mode is the best Emacs mode in existence</title><rect x="525" y="15" opacity="0.8" width="7" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(530,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">greader</text></g></a><a href="https://emacsconf.org/2025/talks/open-mic" title="Open session" data-slug="open-mic"><title> 2:50- 3:40 Open session</title><rect x="549" y="15" opacity="0.8" width="78" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(625,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">open-mic</text></g></a><a href="https://emacsconf.org/2025/talks/sat-close" title="Saturday closing remarks / open session" data-slug="sat-close"><title> 4:00- 4:10 Saturday closing remarks / open session</title><rect x="658" y="15" opacity="0.8" width="15" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(671,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">sat-close</text></g></a><a href="https://emacsconf.org/2025/talks/schemacs" title="One year progress update Schemacs (formerly Gypsum)" data-slug="schemacs"><title> 9:30- 9:55 One year progress update Schemacs (formerly Gypsum)</title><rect x="47" y="75" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="skyblue"></rect><g transform="translate(84,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">schemacs</text></g></a><a href="https://emacsconf.org/2025/talks/juicemacs" title="Juicemacs: exploring speculative JIT compilation for ELisp in Java" data-slug="juicemacs"><title>10:15-10:35 Juicemacs: exploring speculative JIT compilation for ELisp in Java</title><rect x="117" y="75" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="5,5,5" fill="skyblue"></rect><g transform="translate(146,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">juicemacs</text></g></a><a href="https://emacsconf.org/2025/talks/swanky" title="Swanky Python: Interactive development for Python" data-slug="swanky"><title>10:45-11:10 Swanky Python: Interactive development for Python</title><rect x="164" y="75" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="5,5,5" fill="skyblue"></rect><g transform="translate(201,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">swanky</text></g></a><a href="https://emacsconf.org/2025/talks/python" title="Interactive Python programming in Emacs" data-slug="python"><title>11:20-11:40 Interactive Python programming in Emacs</title><rect x="219" y="75" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="" fill="skyblue"></rect><g transform="translate(248,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">python</text></g></a><a href="https://emacsconf.org/2025/talks/llm" title="Emacs, editors, and LLM driven workflows" data-slug="llm"><title> 1:00- 1:25 Emacs, editors, and LLM driven workflows</title><rect x="376" y="75" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="skyblue"></rect><g transform="translate(413,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">llm</text></g></a><a href="https://emacsconf.org/2025/talks/private-ai" title="Emacs and private AI: a great match" data-slug="private-ai"><title> 1:45- 2:05 Emacs and private AI: a great match</title><rect x="447" y="75" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="" fill="skyblue"></rect><g transform="translate(476,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">private-ai</text></g></a><a href="https://emacsconf.org/2025/talks/commonlisp" title="Common Lisp images communicating like-a-human through shared Emacs slime and eev" data-slug="commonlisp"><title> 2:25- 2:55 Common Lisp images communicating like-a-human through shared Emacs slime and eev</title><rect x="509" y="75" opacity="0.8" width="47" height="59" stroke="black" stroke-dasharray="5,5,5" fill="skyblue"></rect><g transform="translate(554,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">commonlisp</text></g></a><a href="https://emacsconf.org/2025/talks/graphics" title="Modern Emacs/Elisp hardware/software accelerated graphics" data-slug="graphics"><title> 3:05- 3:30 Modern Emacs/Elisp hardware/software accelerated graphics</title><rect x="572" y="75" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="skyblue"></rect><g transform="translate(609,133)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">graphics</text></g></a><g transform="translate(0,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">9 AM</text></g><g transform="translate(94,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">10 AM</text></g><g transform="translate(188,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">11 AM</text></g><g transform="translate(282,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">12 PM</text></g><g transform="translate(376,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">1 PM</text></g><g transform="translate(470,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">2 PM</text></g><g transform="translate(564,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">3 PM</text></g><g transform="translate(658,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">4 PM</text></g><g transform="translate(752,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">5 PM</text></g></g><g transform="translate(0,150)"><title>Schedule for Sunday</title><rect width="800" height="150" x="0" y="0" fill="white"></rect><text font-size="10" fill="black" y="12" x="3">Sunday</text><a href="https://emacsconf.org/2025/talks/sun-open" title="Sunday opening remarks" data-slug="sun-open"><title> 9:00- 9:10 Sunday opening remarks</title><rect x="0" y="15" opacity="0.8" width="15" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(13,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">sun-open</text></g></a><a href="https://emacsconf.org/2025/talks/modern" title="Some problems of modernizing Emacs" data-slug="modern"><title> 9:10- 9:30 Some problems of modernizing Emacs</title><rect x="15" y="15" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(44,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">modern</text></g></a><a href="https://emacsconf.org/2025/talks/reader" title="An introduction to the Emacs Reader" data-slug="reader"><title> 9:40-10:15 An introduction to the Emacs Reader</title><rect x="62" y="15" opacity="0.8" width="54" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(114,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">reader</text></g></a><a href="https://emacsconf.org/2025/talks/weights" title="Weightlifting tracking with Emacs on Android" data-slug="weights"><title>10:35-10:45 Weightlifting tracking with Emacs on Android</title><rect x="149" y="15" opacity="0.8" width="15" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(162,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">weights</text></g></a><a href="https://emacsconf.org/2025/talks/completion" title="corfu+yasnippet: Easier than I thought" data-slug="completion"><title>11:05-11:25 corfu+yasnippet: Easier than I thought</title><rect x="196" y="15" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(225,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">completion</text></g></a><a href="https://emacsconf.org/2025/talks/zettelkasten" title="Zettelkasten for regular Emacs hackers" data-slug="zettelkasten"><title> 1:00- 1:25 Zettelkasten for regular Emacs hackers</title><rect x="376" y="15" opacity="0.8" width="39" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(413,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">zettelkasten</text></g></a><a href="https://emacsconf.org/2025/talks/hyperboleqa" title="Questions and answers to help you fly with Hyperbole" data-slug="hyperboleqa"><title> 1:45- 2:15 Questions and answers to help you fly with Hyperbole</title><rect x="447" y="15" opacity="0.8" width="47" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(492,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">hyperboleqa</text></g></a><a href="https://emacsconf.org/2025/talks/gardening" title="Gardening in Emacs: A Windows user's tale of tending, tweaking, and triumph" data-slug="gardening"><title> 2:15- 2:35 Gardening in Emacs: A Windows user's tale of tending, tweaking, and triumph</title><rect x="494" y="15" opacity="0.8" width="31" height="59" stroke="black" stroke-dasharray="5,5,5" fill="peachpuff"></rect><g transform="translate(523,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">gardening</text></g></a><a href="https://emacsconf.org/2025/talks/bookclub-tapas" title="Bookclub tapas" data-slug="bookclub-tapas"><title> 2:45- 3:20 Bookclub tapas</title><rect x="541" y="15" opacity="0.8" width="54" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(593,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">bookclub-tapas</text></g></a><a href="https://emacsconf.org/2025/talks/sun-close" title="Sunday closing remarks" data-slug="sun-close"><title> 3:40- 3:50 Sunday closing remarks</title><rect x="627" y="15" opacity="0.8" width="15" height="59" stroke="black" stroke-dasharray="" fill="peachpuff"></rect><g transform="translate(640,73)"><text fill="black" x="0" y="0" font-size="10" transform="rotate(-90)">sun-close</text></g></a><g transform="translate(0,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">9 AM</text></g><g transform="translate(94,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">10 AM</text></g><g transform="translate(188,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">11 AM</text></g><g transform="translate(282,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">12 PM</text></g><g transform="translate(376,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">1 PM</text></g><g transform="translate(470,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">2 PM</text></g><g transform="translate(564,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">3 PM</text></g><g transform="translate(658,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">4 PM</text></g><g transform="translate(752,3)"><line stroke="darkgray" x1="0" y1="0" x2="0" y2="120"></line><text fill="black" x="0" y="140" font-size="10" text-anchor="left">5 PM</text></g></g></svg>

</figure>

<p>
<mark>Idea:</mark> I still haven't gotten around to localizing times on the watch pages. That could be nice.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-recorded-introductions" class="outline-3">
<h3 id="emacsconf-2025-notes-recorded-introductions">Recorded introductions</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-recorded-introductions">
<p>
Recording all the introductions beforehand was extremely helpful. I used <a href="https://github.com/sachac/subed-record/blob/main/subed-record.el">subed-record.el</a> to record and compile the audio without having to edit out the oopses manually. Recording the intros also gave me something to do other than worry about missing videos.
</p>

<p>
A few speakers helped correct the pronunciations of their names, which was nice. I probably could have picked up the right pronunciation for one of them if I had remembered to check his video first, as he had not only uploaded a video early but even said his name in it. Next time, I can go check that first.
</p>

<p>
This year, all the intros played the properly corrected files along with their subtitles. The process is improving!
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-recorded-videos" class="outline-3">
<h3 id="emacsconf-2025-notes-recorded-videos">Recorded videos</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-recorded-videos">
<p>
As usual, most speakers sent in pre-recorded
videos this year, although it took them a little
bit longer to do them because life got busy for
everyone. Just like last year, speakers uploaded
their files via <a href="https://github.com/psi-4ward/psitransfer">PsiTransfer</a>. I picked up some
people's videos late because I hadn't checked.
I've modified the upload instructions to ask the
speakers to email me when they've uploaded their
file, since I'm not sure that PsiTransfer can
automatically send email when a new file has been
uploaded. We set up a proper domain name, so this
year, people didn't get confused by trying to FTP
to it.
</p>

<p>
I had to redo some of the re-encodings and ask one speaker to reupload is video, but we managed to catch those errors before they streamed.
</p>

<p>
I normalized all the audio myself this year. I used Audacity to normalize it to -16 LUFS. I didn't listen to everything in full, but the results seemed to have been alright. <mark>Idea:</mark> Audio left/right difference was not an issue this year, but in the future, I might still consider mixing the audio down to mono.
</p>

<p>
All the files properly loaded from the cache directory instead of getting shadowed by files in other directories, since I reused the process from last year instead of switching mid-way.
</p>

<p>
People didn't complain about colour smearing, but
it looks like the Calc talk had some. Ah! For some
reason, MPV was back on 0.35, so now I've modified
our Ansible playbook so that it insists on 0.38. I
don't think there were any issues about switching
between dark mode or light mode.
</p>

<p>
There was one last-minute upload where I wasn't sure whether there were supposed to be captions in the first part. When I toggled the captions to try to reload them once I copied over the updated VTT, I think I accidentally left them toggled them off, but fortunately the speaker let me know in IRC.
</p>

<p>
For the opening video, I made the text a bit more generic by removing the year references so that I can theoretically reuse the audio next year.
That might save me some time.
</p>

<p>
I modified our <a href="https://emacsconf.org/mpv/">mpv.conf</a> to display the time remaining in the lower right-hand corner. This was a fantastic idea that made it easy to give the speaker a heads-up that their recorded talk was about to finish and that we were about to switch over to the live Q&amp;A session. Because sometimes we weren't able to spare enough attention to host a session, I usually just added the time of the next session to the Etherpad to give speakers a heads-up that the stream would be switching away from their Q&amp;A session. Then I didn't need to make a fancy Javascript timer either.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-captioning" class="outline-3">
<h3 id="emacsconf-2025-notes-captioning">Captioning</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-captioning">
<p>
The next step in the process was to caption each uploaded video. While speech recognition tools give us a great headstart, there's really no way around the work of getting technical topics right.
</p>

<p>
We used <a href="https://github.com/m-bain/whisperX">WhisperX</a> for speech-to-text again. This time, I used the <code>&#45;&#45;initial_prompt</code> argument
to try to get it to spell things properly: "Transcribe this talk about Emacs. It may mention Emacs keywords such as Org Mode, Org Roam, Magit, gptel, or chatgpt-shell, or tech keywords such as LLMs. Format function names and keyboard shortcut sequences according to Emacs conventions using Markdown syntax. For example: control h becomes \`C-h\`.". It did not actually do the keyboard shortcut sequences. (Emacs documentation is an infinitesimal fraction of the training data, probably!) It generally did a good job of recognizing Emacs rather than EMAX and capitalizing things. <mark>Idea:</mark> I can experiment with few-shot prompting techniques or just expand <code>my-subed-fix-common-errors</code> to detect the patterns.
</p>

<p>
This year, we used Anush V's <a href="https://gitlab.com/jun8git/sub-seg">sub-seg</a> tool to split the subtitles into reasonably short, logically split phrases. I used to manually split these so that the subtitles flowed more smoothly, or if I was pressed for time, I left it at just the length-based splits that WhisperX uses. sub-seg was much nicer as a starting point, so I wrote a shell script to run it more automatically. It still had a few run-on captions and there were a couple of files that confused it, so I manually split those.
</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>Shell script for calling sub-seg</strong></summary>
<div class="nil" id="org8393d94">
<p>
#!/bin/bash
~/vendor/sub-seg/.venv/bin/python3 ~/vendor/sub-seg/src/prediction.py "$1" <i>tmp/subseg-predict.txt
~/vendor/sub-seg</i>.venv/bin/python3 ~/vendor/sub-seg/src/postprocess_to_single_lines.py "\(1" /tmp/subseg-predict.txt /tmp/subseg-cleaned.txt
grep -v -e "^\)" /tmp/subseg-cleaned.txt &gt; "${1%.*}&ndash;backstage&ndash;split.txt"
</p>

</div>


</details>

<p>
I felt that it was also a good idea to correct the timing before posting the subtitles for other people to work on because otherwise it would be more difficult for any volunteers to be able to replay parts of the subtitles that needed extra attention. WhisperX often gave me overlapping caption timestamps and it didn't always have word timestamps I could use instead. I used <a href="https://www.readbeyond.it/aeneas/">Aeneas</a> to re-align the text with the audio so that we could get better timestamps. As usual, Aeneas got confused by silences or non-speech audio, so I used my <a href="https://github.com/sachac/subed/blob/main/subed/subed-word-data.el">subed-word-data.el</a> code to visualize the word timing and my <a href="https://github.com/sachac/subed/blob/main/subed/subed-align.el">subed-align.el</a> code to realign regions. <mark>Idea:</mark> I should probably be able to use the word data to realign things semi-automatically, or at least flag things that might need more tweaking. There were a few chunks that weren't recognized at all, but I was able to spot them and fix them when I was fixing the timestamps. Making this more automated will make it much easier to share captioning work with other volunteers. Alternatively, I can also post the talks for captioning before I fix the timestamps, because I can fix them while other people are editing the text.
</p>

<p>
While I was correcting the timestamps, it was pretty tempting for me to just go ahead and fix whatever errors I encountered along the way. From there on, it seemed like only a little bit more work was needed in order to get it ready for the speaker to review, and besides, doing subtitles is one of the things I enjoy about EmacsConf. So the end result is that for most of the talks, by the time I finished getting it to the point where I felt like someone who was new to captioning could take over, it was often quite close to being done. I did actually manage to successfully accept some people's help. The code that I wrote for showing me the word differences between two files (sometimes the speaker's script or the subtitles that were edited by another volunteer) was very useful for last-minute merges.
</p>

<p>
Working on captions is one of my favourite parts. Well-edited captions are totally a nice-to-have, but I like it when people can watch the videos (or skim them) and not worry about not being able to hear or understand what someone said. I enjoy sitting down and spending time with people's presentations, turning them into text that we can search. Captioning helps me feel connected with some of the things I love the most about the Emacs community.
</p>

<p>
I really appreciated how a number of speakers and volunteers helped with quality control by watching other videos in the backstage area. <mark>Idea:</mark> I can spend some time improving the design of the backstage area to make it more pleasant and to make it easier for people to find something to work on if they have a little time.
</p>

<p>
Of course, some talks couldn't be captioned beforehand because they were live. For live conferences and for many of the Q&amp;A sessions, we used BigBlueButton.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-bigbluebutton-web-conference" class="outline-3">
<h3 id="emacsconf-2025-notes-bigbluebutton-web-conference">BigBlueButton web conference</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-bigbluebutton-web-conference">
<p>
We used <a href="https://bigbluebutton.org">BigBlueButton</a> again this year instead of switching over to something like Galene. I moved the node from bandali's Linode account to mine so that I wouldn't feel as guilty bringing it up and down throughout the year for various Emacs meetups. That also meant that the actual setup was fairly stable since it had survived a lot of meetups before EmacsConf, and I didn't have to spend as much time before the event worrying about it. I still worried a little, as after the November OrgMeetup, I noticed a technical issue that I couldn't nail down. Fortunately, that happened after the meetup had already ended. I also used my meetup crontab to schedule test sessions with some speakers so that they could record their presentation or doublecheck their sharing setup.
</p>

<p>
I resized the BigBlueButton early Friday afternoon this year so that speakers could have a few more hours to test their setups if they wanted to, especially since one speaker had mentioned having a hard time sharing his screen on Wayland.
</p>

<p>
I thought I had <code>&#45;&#45;allow_auto_disk_resize true</code> in the <code>bbb-prod</code> shell-script I used to resize the node, but I don't think <code>linode-cli</code> actually resized the disk (maybe I ran a different shell script), and I forgot to double-check that before the conference. Fortunately, we had just enough space for all the recordings. I noticed only when I was waiting for the recordings to finish processing, since some of them were failing. <mark>Idea:</mark> Next time, I should manually check that the disk has resized, and I can probably tweak my checklist too.
</p>

<p>
Like before, we used moderator codes so that speakers could let themselves into the room and get everything set up just in case. <mark>Idea:</mark> I wonder if there's a way to specify the moderator code in the URL so that I can link to that directly. Maybe I can modify the web interface to fill that in with a little bit of Javascript.
</p>

<p>
Oddly, the moderator codes didn't let people start or stop recording, but fortunately I was able to log in and take care of all of that. I mostly remembered to start the recording, except for one instance where I started recording a little bit late.
</p>

<details><summary>Code for reporting BBB usage</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(emacsconf-extract-bbb-report
 (seq-remove (<span class="org-keyword">lambda</span> (o)
               (string-match <span class="org-string">"backups"</span> o))
             (directory-files-recursively emacsconf-extract-bbb-raw-dir <span class="org-string">"events.xml"</span>)))
</code></pre>
</div>

</details>

<table>


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

<col class="org-right">

<col class="org-right">

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">2023</th>
<th scope="col" class="org-right">2024</th>
<th scope="col" class="org-right">2025</th>
<th scope="col" class="org-left">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">62</td>
<td class="org-right">107</td>
<td class="org-right">35</td>
<td class="org-left">Max number of simultaneous users</td>
</tr>

<tr>
<td class="org-right">6</td>
<td class="org-right">7</td>
<td class="org-right">5</td>
<td class="org-left">Max number of simultaneous meetings</td>
</tr>

<tr>
<td class="org-right">27</td>
<td class="org-right">25</td>
<td class="org-right">14</td>
<td class="org-left">Max number of people in one meeting</td>
</tr>

<tr>
<td class="org-right">84</td>
<td class="org-right">102</td>
<td class="org-right">55</td>
<td class="org-left">Total unique users</td>
</tr>

<tr>
<td class="org-right">36</td>
<td class="org-right">40</td>
<td class="org-right">27</td>
<td class="org-left">Total unique talking</td>
</tr>
</tbody>
</table>

<p>
I might be able to get away with a Linode 8 GB node (USD 0.072/hour) instead of a Linode 16 GB node (USD 0.144/hour), but keeping it at the current size is probably okay.
</p>

<p>
I'll go into more detail in the budget section, but the total was USD 14.02 to host BigBlueButton ourselves in December, and between USD 5 to USD 8 / month to run it the rest of the time (including upsizing it for meetups), so that was much less than what it would have cost for BigBlueButton-specific hosting.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-checking-speakers-in" class="outline-3">
<h3 id="emacsconf-2025-notes-checking-speakers-in">Checking speakers in</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-checking-speakers-in">
<p>
To keep the live talks and Q&amp;A sessions flowing smoothly, there's a bit of a scramble behind the scenes. We asked speakers to check in at least half an hour before they need to go live, and most of them were able to find their way to the BigBlueButton room using the provided moderator codes. I mostly handled the check-ins, with some help from Corwin when two people needed to be checked in at the same time. We generally didn't have any tech issues, although a couple of people were missing during their Q&amp;A sessions. (We just shrugged and continued; too much to take care of to worry about anything!)
</p>

<p>
Check-ins are usually good opportunities to chat with speakers, but because I needed to pay attention to the other streams as well as check in other people, it felt a bit more rushed and less enjoyable. I missed having those opportunities to connect, but I'm glad speakers were able to handle so much on their own.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-hosting" class="outline-3">
<h3 id="emacsconf-2025-notes-hosting">Hosting</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-hosting">
<p>
Corwin did a lot of the on-stream hosting with a little help from Amin on Saturday. I occasionally jumped in and asked the speakers questions from the pad, but when I was busy checking people in or managing other technical issues cropping up, the speakers also read the questions out themselves so that I could match things up in the transcript afterwards.
</p>

<p>
No crashes this time, I think. Hooray!
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-infrastructure" class="outline-3">
<h3 id="emacsconf-2025-notes-infrastructure">Infrastructure</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-infrastructure">
<p>
I used September and October to review all the infrastructure and see what made sense to upgrade. The BigBlueButton instance is on a virtual private server that's dedicated to it, since it doesn't like to share with any other services. Since I set it up from scratch following the recommended configuration, the server uses a recent version of Ubuntu. Some of the other servers use outdated Debian distributions that no longer get security updates. They have other services on them, so I'll leave them to bandali to upgrade when he's ready.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-streaming" class="outline-3">
<h3 id="emacsconf-2025-notes-streaming">Streaming</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-streaming">
<p>
I am so, so glad that we had our <a href="https://en.wikipedia.org/wiki/VNC">VNC</a>+<a href="https://obsproject.com/">OBS</a> setup this year, with two separate user accounts using OBS to stream each track from a virtual desktop on a remote server. We switched to this setup in 2022 so that I could handle multiple tracks without worrying about last-minute coordination or people's laptop specs. If we had had our original setup in 2019, with hosts streaming from their personal computers, I think we'd have been dead in the water. Instead, our scripts took care of most of the on-screen actions, so I just needed to rearrange the layout. (<mark>Idea:</mark> If I can figure out how to get BigBlueButton to use our preferred layout, that would make it even better.)
</p>

<p>
I used MPV to monitor the streams in little thumbnails. I set those windows to always be on top, and I set the audio so that the general track was on my left side and the development track was o my right. I used some shortcuts to jump between streams reliably, taking advantage of how I bound the Windows key on my laptop to the Super modifier. I used KDE's custom keyboard shortcuts to set Super-g to raise all of my general-track windows and Super-d to do the same for all of the development-track ones. Those were set to commands like this:
</p>

<p>
<code>/home/sacha/bin/focus_or_launch "emacsconf-gen - TigerVNC"; /home/sacha/bin/focus_or_launch gen.webm; /home/sacha/bin/focus_or_launch "gen.*Konsole"</code>
</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 focus_or_launch</strong></summary>
<div class="nil" id="org00546ea">
<p>
#!/bin/bash
</p>


<p>
############# GLOBVAR/PREP ###############
</p>

<p>
Executable="\(1"
ExecutableBase="\)(basename "$Executable")"
Launchcommand="$2"
Usage="\
Usage: $(basename $0) command [launchcommand] [exclude]
E.g.:  $(basename $0) google-chrome\
"
ExcludePattern="$3"
</p>

<p>
################ MAIN ####################
</p>

<p>
if ; then
    MostRecentWID="$(printf "%d" $(wmctrl -xl | grep "$ExecutableBase" | tail -1 2&gt; /dev/null | awk '{print \(1}'))"
else
    MostRecentWID="\)(printf "%d" $(wmctrl -xl | grep -ve "$ExcludePattern" | grep "$ExecutableBase" | tail -1 2&gt; /dev/null | awk '{print $1}'))"
fi
</p>

<p>
if ; then
    if ; then
        "$Executable" &gt; /dev/null 2&gt;&amp;1 &amp;
    else
        $LaunchCommand &gt; /dev/null 2&gt;&amp;1 &amp;
    fi
    disown
else
    if xdotool search &ndash;class "\(ExecutableBase" | grep -q "^\)(xdotool getactivewindow)$"; then
       xdotool sleep 0.050 key "ctrl+alt+l"
    else
</p>

<p>
        xdotool windowactivate "$MostRecentWID" 2&gt;&amp;1 | grep failed \
            &amp;&amp; xdotool search &ndash;class "$ExecutableBase" windowactivate %@
    fi
fi
</p>

</div>


</details>

<p>
(<mark>Idea:</mark> It would be great to easily assign specific windows to shortcuts on my numeric keypad or on a macropad. Maybe I can rename a window or manually update a list that a script can read&hellip;)
</p>

<p>
To get the video streams out to viewers, we used <a href="http://icecast.org/">Icecast (a streaming media server)</a> on a Linode 64GB 16 core shared CPU server again this year, and that seemed to work out. I briefly contemplated using a more modern setup like <a href="https://docs.nginx.com/nginx/admin-guide/dynamic-modules/rtmp/">nginx-rtmp</a>, <a href="https://antmedia.io/">Ant Media Server</a>, or <a href="https://ossrs.net/lts/en-us/">SRS</a> if we wanted HLS (wider support), adaptive bitrate streaming, or lower latency, but I decided against it because I didn't want to add more complexity. Good thing too. This year would not have been a good time to experiment with something new.
</p>

<p>
Like before, watching the stream directly using mpv, vlc, or ffplay was smoother than watching it through the web-based viewers. One of the participants suggested adding more detailed instructions for VLC so that people can enjoy it even without using the command-line, so we did.
</p>

<p>
The 480p stream and the YouTube stream were all right, although I forgot to start one of the YouTube streams until a bit later. <mark>Idea:</mark> Next time, I can set all the streams to autostart.
</p>

<p>
Corwin had an extra server lying around, so I used that to restream to Toobnix just in case. That seems to have worked, although I didn't have the brainspace to check on it.
</p>

<p>
I totally forgot about displaying random packages on the waiting screen. <mark>Idea:</mark> Maybe next year I can add a fortune.txt to the cache directory with various one-line Emacs tips.
</p>

<p>
I might be able to drop the specifications down to 32GB 8 core if we wanted to.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-publishing" class="outline-3">
<h3 id="emacsconf-2025-notes-publishing">Publishing</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-publishing">
<p>
People appreciated being able to get videos and transcripts as soon as each talk aired. Publishing the video files and transcripts generally worked out smoothly, aside from a few times when I needed to manually update the git repository. I modified the code to add more links to the <a href="https://media.emacsconf.org/2025/schedules/">Org files</a> and to walk me through publishing videos to YouTube.
</p>

<p>
I uploaded all the videos to <a href="https://www.youtube.com/@EmacsConf">YouTube</a> and scheduled them so that I didn't have to manage that during the conference. I did not get around to uploading them to <a href="https://toobnix.org/c/emacsconf/videos">Toobnix</a> until after the conference since dealing with lots of duplicated updates is annoying. I've been writing a few more functions to work with the Toobnix API, so it might be a little easier to do things like update descriptions or subtitles next year.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-etherpad" class="outline-3">
<h3 id="emacsconf-2025-notes-etherpad">Etherpad</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-etherpad">
<p>
As usual, we used <a href="https://etherpad.org/">Etherpad</a> to collect questions from conference partcipants, and many speakers answered questions there as well.
</p>

<p>
I used the <a href="https://galaxy.ansible.com/ui/standalone/roles/systemli/etherpad/">systemli.etherpad</a> Ansible role to upgrade to Etherpad 2.5. This included some security fixes, so that was a relief.
</p>

<p>
I added pronouns and pronunciations to the Etherpad to make it easier for hosts to remember just in case.
</p>

<p>
I did not get to use <code>emacsconf-erc-copy</code> to copy IRC to Etherpad because I was too busy juggling everything else, so I just updated things afterwards.
</p>

<p>
<mark>Idea:</mark> Just in case, it might be good to have a backup plan in case I need to switch Etherpad to authenticated users or read-only use. Maybe I can prepare questions beforehand, just in case we get some serious griefing.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-irc" class="outline-3">
<h3 id="emacsconf-2025-notes-irc">IRC</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-irc">
<p>
The plain-text chat channels on IRC continued to be a great place to discuss things, with lots of discussions, comments, and encouraging feedback. My automated IRC system continued to do a good job of posting the talk links, and the announcements made it easier to split up the log by talk.
</p>

<p>
Planning for social challenges is harder than planning for technical ones, though.
libera.chat has been dealing with spam attacks recently. Someone's also been griefing #emacs and other channels via the chat.emacsconf.org web interface, so we decided to keep chat.emacsconf.org dormant until the conference itself. If this is an issue next year, we might need to figure out moderation. I'd prefer to not require everyone to register accounts or be granted voice permissions, so we'll see.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-extracting-the-q-a" class="outline-3">
<h3 id="emacsconf-2025-notes-extracting-the-q-a">Extracting the Q&amp;A</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-extracting-the-q-a">
<p>
We recorded all the Q&amp;A sessions so that we could post them afterwards. As mentioned, I only started the recording late once. Progress! I generally started it a few minutes early. As I got more confident about paying attention to the start of a session and rearranging the layout on screen, I also got better at starting the recording shortly before turning it over to the speaker. That made it easier to trim the recordings afterwards.
</p>

<p>
It took me a little longer to get to the Q&amp;A sessions this year. Like last year, getting the recordings from BigBlueButton was fairly easy because I could get a single processed video instead of combining the audio, screenshare, and webcam video myself. This year the single-file video downloads were .m4v files, so I needed to modify things slightly. I think my workflow last year assumed I started with .webm files. Anyway, after some re-encoding, I got it processed. Now I've modified the <code>/usr/local/bigbluebutton/core/scripts/video.yml</code> to enable webm. I wonder if it got wiped after my panic-reinstall after November's OrgMeetup. <mark>Idea:</mark> I should add the config to my Ansible so that it stays throughout reinstalls and upgrades.
</p>

<p>
I wrote a <code>emacsconf-subed-copy-current-chapter-text</code> function so that I can more easily paste answers into the Q&amp;A&hellip; which turned out to be a superset of the <code>emacsconf-extract-subed-copy-section-text</code> I mentioned in last year's notes and totally forgot about. This tells me that I need to add it to my <a href="https://emacsconf.org/organizers-notebook/">EmacsConf - organizers-notebook</a> notes so that I actually know what to do. I did not use my completion functions to add section headings based on comments either. Apparently this was <code>emacsconf-extract-insert-note-with-question-heading</code>, which I do have a note about in my organizer notebook.
</p>

<p>
Audio mixing was reasonable. I normalized the audio in Audacity, and I manually fixed some sections where some participants' audio volumes were lower.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-budget" class="outline-3">
<h3 id="emacsconf-2025-notes-budget">Budget</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-budget">
<p>
Hosting a conference online continues to be pretty manageable. Here are our costs for the year (including taxes where applicable):
</p>

<details><summary>Make a CSV with the invoice data</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">with-temp-file</span> <span class="org-string">"~/proj/emacsconf/2025/all-invoices.csv"</span>
  (insert
   <span class="org-string">"\"Description\",\"From\",\"To\",\"Quantity\",\"Region\",\"Unit Price\",\"Amount (USD)\",\"Tax (USD)\",\"Total (USD)\"\n"</span>
   (orgtbl-to-csv
    (seq-mapcat
     (<span class="org-keyword">lambda</span> (file)
       (seq-filter
        (<span class="org-keyword">lambda</span> (row)
          (string-match <span class="org-string">"meet</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">front0</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">live0"</span> (car row)))
        (cdr (pcsv-parse-file file))))
     (directory-files-recursively <span class="org-string">"~/proj/emacsconf/2025/invoices/"</span>
                                  <span class="org-string">"\\.csv"</span>))
    nil)))
</code></pre>
</div>

</details>
<details><summary>Code for calculating BBB node costs</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(append
<span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"Node"</span> <span class="org-string">"Jan"</span> <span class="org-string">"Feb"</span> <span class="org-string">"Mar"</span> <span class="org-string">"Apr"</span> <span class="org-string">"May"</span> <span class="org-string">"Jun"</span> <span class="org-string">"Jul"</span> <span class="org-string">"Aug"</span> <span class="org-string">"Sep"</span> <span class="org-string">"Oct"</span> <span class="org-string">"Nov"</span> <span class="org-string">"Dec"</span> <span class="org-string">"Total"</span>) hline)
(mapcar
 (<span class="org-keyword">lambda</span> (group)
   (<span class="org-keyword">let</span> ((amounts (mapcar
          (<span class="org-keyword">lambda</span> (file)
            (format <span class="org-string">"%.2f"</span>
                    (apply <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">+</span>
                           (seq-keep (<span class="org-keyword">lambda</span> (row)
                                       (<span class="org-keyword">when</span> (string-match (car group) (car row))
                                         (string-to-number (elt row 8))))
                                     (cdr (pcsv-parse-file file))))))
          (directory-files (format <span class="org-string">"~/proj/emacsconf/2025/invoices/%s/"</span> (cdr group))
                           t <span class="org-string">"\\.csv"</span>))))
   (append (list (car group))
           amounts
           (list (format <span class="org-string">"%.2f"</span> (apply <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">+</span> (mapcar <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">string-to-number</span> amounts)))))))
 <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"meet"</span> . <span class="org-string">"sacha"</span>)
   (<span class="org-string">"front0"</span> . <span class="org-string">"bandali"</span>)
   (<span class="org-string">"live0"</span> . <span class="org-string">"bandali"</span>)))
   nil
   )
</code></pre>
</div>

</details>

<table>


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

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Node</th>
<th scope="col" class="org-right">Jan</th>
<th scope="col" class="org-right">Feb</th>
<th scope="col" class="org-right">Mar</th>
<th scope="col" class="org-right">Apr</th>
<th scope="col" class="org-right">May</th>
<th scope="col" class="org-right">Jun</th>
<th scope="col" class="org-right">Jul</th>
<th scope="col" class="org-right">Aug</th>
<th scope="col" class="org-right">Sep</th>
<th scope="col" class="org-right">Oct</th>
<th scope="col" class="org-right">Nov</th>
<th scope="col" class="org-right">Dec</th>
<th scope="col" class="org-right">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">meet</td>
<td class="org-right">2.17</td>
<td class="org-right">7.55</td>
<td class="org-right">6.78</td>
<td class="org-right">6.74</td>
<td class="org-right">7.13</td>
<td class="org-right">6.95</td>
<td class="org-right">7.19</td>
<td class="org-right">7.27</td>
<td class="org-right">6.75</td>
<td class="org-right">7.19</td>
<td class="org-right">7.56</td>
<td class="org-right">14.02</td>
<td class="org-right">87.30</td>
</tr>

<tr>
<td class="org-left">front0</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">18.79</td>
<td class="org-right">73.79</td>
</tr>

<tr>
<td class="org-left">live0</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">5.00</td>
<td class="org-right">32.89</td>
<td class="org-right">87.89</td>
</tr>
</tbody>
</table>

<p>
Grand total for 2025: USD 248.98
</p>

<p>
Server configuration during the conference:
</p>

<table>


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

<col class="org-left">

<col class="org-right">

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Node</th>
<th scope="col" class="org-left">Specs</th>
<th scope="col" class="org-right">Hours upscaled</th>
<th scope="col" class="org-left">CPU load</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">meet</td>
<td class="org-left">16GB 8core shared</td>
<td class="org-right">52</td>
<td class="org-left">peak 172% CPU (100% is 1 CPU)</td>
</tr>

<tr>
<td class="org-left">front</td>
<td class="org-left">32GB 8core shared</td>
<td class="org-right">47</td>
<td class="org-left">peak 47% CPU (100% is 1 CPU)</td>
</tr>

<tr>
<td class="org-left">live</td>
<td class="org-left">64GB 16core shared</td>
<td class="org-right">48</td>
<td class="org-left">peak 440% CPU (100% is 1 CPU)</td>
</tr>

<tr>
<td class="org-left">res</td>
<td class="org-left">46GB 12core</td>
<td class="org-right">&nbsp;</td>
<td class="org-left">peak 60% total CPU (100% is 12 CPUs); each OBS ~3.5 CPUs), mem 7GB used</td>
</tr>

<tr>
<td class="org-left">media</td>
<td class="org-left">3GB 1core</td>
<td class="org-right">&nbsp;</td>
<td class="org-left">&nbsp;</td>
</tr>
</tbody>
</table>

<p>
These hosting costs are a little higher than 2024 because we now pay for hosting the BigBlueButton server (meet) year-round. That's ~ 5 USD/month for a Linode 1 GB instance and an extra USD 2-3 / month that lets us provide a platform for OrgMeetup, Emacs APAC, and Emacs Berlin by alternating between a 1 GB Linode and an 8 GB Linode as needed. The meetups had previously been using the free <a href="https://meet.jit.si/">Jitsi</a> service, but sometimes that has some limitations. In 2023 and most of 2024, the BigBlueButton server had been provided by FossHost, which has since shut down. This time, maintaining the server year-round meant that we didn't have to do any last-minute scrambles to install and configure the machine, which I appreciated. I could potentially get the cost down further if I use Linode's custom images to create a node from a saved image right before a meetup, but I think that trades about USD 2 of savings/month for much more technical risk, so I'm fine with just leaving it running downscaled.
</p>

<p>
If we need to cut costs, live0 might be more of a candidate because I think we'll be able to use Ansible scripts to recreate the Icecast setup. I think we're fine, though. Also, based on the CPU peak loads, we might be able to get away with lower specs during the conference (maybe meet: 8 GB, front: 8 GB, live: 32 GB), for an estimated savings of USD 27.76, with the risk of having to worry about it if we hit the limit. So it's probably not a big deal for now.
</p>

<p>
I think people's donations through the Working Together program can cover the costs for this year, just like last year. (Thanks!) I just have to do some paperwork.
</p>

<p>
In addition to these servers, Ry P provided res.emacsconf.org for OBS streaming over VNC sessions.
The Free Software Foundation also provided <a href="https://media.emacsconf.org">media.emacsconf.org</a> for serving media files, and yang3 provided <a href="https://eu.media.emacsconf.org">eu.media.emacsconf.org</a>.
</p>

<p>
If other people are considering running an online conference, the hosting part is surprisingly manageable, at least for our tiny audience size of about 100 peak simultaneous viewers and 35 web conference participants. It was nice to make sure that everyone can watch without ads or Javascript.
</p>

<p>
Behind the scenes: In terms of keeping an eye on performance limits, we're usually more CPU-bound than memory- or disk-bound, so we had plenty of room this year. Now I have some Org Babel source blocks to automatically collect the stats from different servers. Here's what that Org block looks like:
</p>


<div class="org-src-container">
<pre class="src src-org"><code><span class="org-org-block-begin-line">#+begin_src sh :dir /ssh:res:/ :results verbatim :wrap example</span>
<span class="org-org-block">top -b -n 1 | head</span>
<span class="org-org-block-end-line">#+end_src</span>
</code></pre>
</div>


<p>
I should write something similar to grab the Icecast stats from  <a href="http://live0.emacsconf.org:8001/status.xsl">http://live0.emacsconf.org:8001/status.xsl</a> periodically. Curl will do the trick, of course, but maybe I can get Emacs to add a row to an Org Mode table.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-documentation-and-time" class="outline-3">
<h3 id="emacsconf-2025-notes-documentation-and-time">Tracking my time</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-documentation-and-time">
<p>
As part of my general interest in time-tracking, I tracked EmacsConf-related time separately from my general Emacs-related time.
</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>Calculations</strong></summary>

<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org806fada"><code>(append (list <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"Year"</span> <span class="org-string">"Month"</span> <span class="org-string">"Hours"</span>) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">hline</span>)
(seq-mapcat
 (<span class="org-keyword">lambda</span> (year)
   (mapcar
    (<span class="org-keyword">lambda</span> (group)
      (list
       year
       (car group)
       (format <span class="org-string">"%.1f"</span>
               (apply <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">+</span>
                      (mapcar (<span class="org-keyword">lambda</span> (o) (/ (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">duration</span> o) 3600.0))
                              (cdr group))))))
    (seq-group-by (<span class="org-keyword">lambda</span> (o)
                    (substring (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">date</span> o) 5 7))
                  (quantified-records
                   (concat year <span class="org-string">"-01-01"</span>)
                   (concat year <span class="org-string">"-12-31"</span>)
                   <span class="org-string">"&amp;filter_string=emacsconf&amp;split=keep&amp;order=oldest"</span>))))
 <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"2024"</span> <span class="org-string">"2025"</span>)))
</code></pre>
</div>


<table id="org61d663b">


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

<col class="org-right">

<col class="org-right">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Year</th>
<th scope="col" class="org-right">Month</th>
<th scope="col" class="org-right">Hours</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">2024</td>
<td class="org-right">01</td>
<td class="org-right">2.5</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">07</td>
<td class="org-right">2.8</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">08</td>
<td class="org-right">0.6</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">09</td>
<td class="org-right">7.0</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">10</td>
<td class="org-right">14.4</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">11</td>
<td class="org-right">30.7</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">12</td>
<td class="org-right">46.2</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">01</td>
<td class="org-right">9.7</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">02</td>
<td class="org-right">1.4</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">04</td>
<td class="org-right">0.9</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">06</td>
<td class="org-right">0.8</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">07</td>
<td class="org-right">6.0</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">08</td>
<td class="org-right">3.6</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">09</td>
<td class="org-right">10.9</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">10</td>
<td class="org-right">1.2</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">11</td>
<td class="org-right">21.0</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">12</td>
<td class="org-right">64.3</td>
</tr>
</tbody>
</table>


<div class="org-src-container">
<pre class="src src-python" id="org363917d"><code><span class="org-keyword">import</span> pandas <span class="org-keyword">as</span> pd
<span class="org-keyword">import</span> matplotlib.pyplot <span class="org-keyword">as</span> plt
<span class="org-variable-name">df</span> <span class="org-operator">=</span> pd.DataFrame(data[1:], columns<span class="org-operator">=</span>data[0])
<span class="org-variable-name">df</span> <span class="org-operator">=</span> pd.pivot_table(df, columns<span class="org-operator">=</span>[<span class="org-string">'Month'</span>], index<span class="org-operator">=</span>[<span class="org-string">'Year'</span>], values<span class="org-operator">=</span><span class="org-string">'Hours'</span>, aggfunc<span class="org-operator">=</span><span class="org-string">'sum'</span>, fill_value<span class="org-operator">=</span>0)
<span class="org-variable-name">df</span> <span class="org-operator">=</span> df.reindex(columns<span class="org-operator">=</span><span class="org-builtin">range</span>(1, 13), fill_value<span class="org-operator">=</span>0)
<span class="org-variable-name">df</span>[<span class="org-string">'Total'</span>] <span class="org-operator">=</span> df.<span class="org-builtin">sum</span>(axis<span class="org-operator">=</span>1)
<span class="org-variable-name">df</span> <span class="org-operator">=</span> df.applymap(<span class="org-keyword">lambda</span> x: f<span class="org-string">"</span>{x:.1f}<span class="org-string">"</span> <span class="org-keyword">if</span> x <span class="org-operator">!=</span> 0 <span class="org-keyword">else</span> <span class="org-string">""</span>)
<span class="org-keyword">return</span> df
</code></pre>
</div>




</details>

<table>


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

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Year</th>
<th scope="col" class="org-right">1</th>
<th scope="col" class="org-right">2</th>
<th scope="col" class="org-right">3</th>
<th scope="col" class="org-right">4</th>
<th scope="col" class="org-right">5</th>
<th scope="col" class="org-right">6</th>
<th scope="col" class="org-right">7</th>
<th scope="col" class="org-right">8</th>
<th scope="col" class="org-right">9</th>
<th scope="col" class="org-right">10</th>
<th scope="col" class="org-right">11</th>
<th scope="col" class="org-right">12</th>
<th scope="col" class="org-right">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">2024</td>
<td class="org-right">2.5</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">2.8</td>
<td class="org-right">0.6</td>
<td class="org-right">7.0</td>
<td class="org-right">14.4</td>
<td class="org-right">30.7</td>
<td class="org-right">46.2</td>
<td class="org-right">104.2</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">9.7</td>
<td class="org-right">1.4</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">0.9</td>
<td class="org-right">&nbsp;</td>
<td class="org-right">0.8</td>
<td class="org-right">6.0</td>
<td class="org-right">3.6</td>
<td class="org-right">10.9</td>
<td class="org-right">1.2</td>
<td class="org-right">21.0</td>
<td class="org-right">64.3</td>
<td class="org-right">119.8</td>
</tr>
</tbody>
</table>


<p>
This year, I spent more time doing the reencoding and captions in December, since most of the submissions came in around that time. I didn't even listen to all the videos in real time. I used my shortcuts to skim the captions and jump around. It would have been nice to be able to spread out the work a little bit more instead of squeezing most of it into the first week of December, since that would have made it easier to coordinate with other volunteers without feeling like I might have to do more last-minute scrambles if they were busier than expected.
</p>

<p>
I was a little bit nervous about making sure that the infrastructure was all going to be okay for the conference as well as double checking the videos for possible encoding issues or audio issues.
Stress tends to make me gloss over things or make small mistakes, which meant I had to slow down even more to double-check things and add more things to our process documentation.
</p>

<p>
I'm glad it all worked out. Even if I switched that time to working on Emacs stuff myself, I don't think I would have been able to put together all the kinds of wonderful tips and experiences that other people shared in the conference, so that was worthwhile.
</p>

<p>
(Here's a <a href="https://sachachua.com/blog/2023/12/analyzing-my-emacs-time-over-the-last-11-years-or-so/">longer-term analysis time going back to 2012</a>.)
</p>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-thanks" class="outline-3">
<h3 id="emacsconf-2025-notes-thanks">Thanks</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-thanks">
<ul class="org-ul">
<li>Thank you to all the speakers, volunteers, and participants, and
to all those other people in our lives who make it possible
through time and support.</li>
<li>Thanks to other volunteers: 
<ul class="org-ul">
<li>Corwin and Amin for helping with the organization</li>
<li>JC Helary, Triko, and James Endres Howell for help reviewing
CFPs</li>
<li>Amitav Krishna, Rodion Goritskov, jay_bird, and indra for
captions</li>
<li>yang3 for the EU mirror we're setting up</li>
<li>Bhavin Gandhi, Michael Kokosenski, Iain Young, Jamie Cullen,
Ihor Radchenko (yantar92), FlowyCoder for other help</li>
</ul></li>
<li>Thanks to the Free Software Foundation for the mailing lists,
the media server, and of course, GNU Emacs.</li>
<li>Thanks to Ry P for the server that we're using for OBS
streaming and processing videos.</li>
<li>Thanks to the many users and contributers and project teams that
create all the awesome free software we use, especially:
<ul class="org-ul">
<li><a href="https://www.gnu.org/software/emacs/">Emacs</a>, <a href="https://orgmode.org/">Org Mode</a>, <a href="https://www.gnu.org/software/emacs/erc.html">ERC</a>, <a href="https://www.gnu.org/software/tramp/">TRAMP</a>, <a href="https://magit.vc/">Magit</a>, <a href="https://bigbluebutton.org">BigBlueButton</a>, <a href="https://etherpad.org/">Etherpad</a>,
<a href="https://ikiwiki.info/">Ikiwiki</a>, <a href="http://icecast.org/">Icecast</a>, <a href="https://obsproject.com/">OBS</a>, <a href="https://github.com/thelounge/thelounge">TheLounge</a>, <a href="https://libera.chat/">libera.chat</a>, <a href="https://www.ffmpeg.org/">ffmpeg</a>,
<a href="https://github.com/openai/whisper">OpenAI Whisper</a>, <a href="https://github.com/m-bain/whisperX">WhisperX</a>, the <a href="https://github.com/readbeyond/aeneas">aeneas</a> forced alignment tool,
<a href="https://github.com/psi-4ward/psitransfer">PsiTransfer</a>, <a href="https://github.com/sachac/subed">subed</a>, <a href="https://gitlab.com/jun8git/sub-seg">sub-seg</a>, <a href="https://www.firefox.com/">Mozilla Firefox</a>, <a href="https://mpv.io/">mpv</a>,
<a href="https://www.tampermonkey.net/">Tampermonkey</a></li>
<li>And many, many other tools and services we used to prepare
and host this years conference</li>
</ul></li>
<li>Thanks to <a href="https://cicadas.surf/~shoshin/">shoshin</a> for the music.</li>
<li>Thanks to people who donated via the <a href="https://my.fsf.org/civicrm/contribute/transact?reset=1&amp;id=70">FSF Working Together program</a>: Scott Ranby, Jonathan Mitchell, and 8 other anonymous donors!</li>
</ul>
</div>
</div>
<div id="outline-container-emacsconf-2025-notes-overall" class="outline-3">
<h3 id="emacsconf-2025-notes-overall">Overall</h3>
<div class="outline-text-3" id="text-emacsconf-2025-notes-overall">
<p>
I've already started hearing from people who
enjoyed the conference and picked up lots of good
tips from it. Wonderful!
</p>

<p>
EmacsConf 2025 was a bit more challenging this
year. The world has gotten a lot busier.
Return-to-work mandates, job market turmoil,
health challenges, bigger societal changes&hellip; It's
harder for people to find time. For example, the
maintainers of Emacs and Org are too busy working
on useful updates and bugfixes to rehash the news
for us, so maybe I'll get back to doing Emacs News
Highlights next year. I'm super lucky in that I
can stand outside most of all of that and make
this space where we can take a break and chat
about Emacs. I really appreciated having this
nice, cozy little place where people could get
together and bump into other people who might be
interested in similar things, either at the event
itself or in the discussions afterwards. I'm glad
we started with a large schedule and let things
settle down. I'd rather have that slack instead of
making speakers feel stressed or guilty.
</p>

<p>
I love the way that working on the various parts
of the conference gives me an excuse to tinker with Emacs and figure out how to use it for more things.
</p>

<p>
As you can tell from the other sections in this
post, I tend to have so much fun doing this that I
often forget to check if I've already written the
same functions before.
When I do remember, I feel really good about being
able to build on this accumulation of little
improvements.
</p>

<p>
I didn't feel like I really got to attend EmacsConf this year, but that's not really surprising because I don't usually get to. I think the only time I've actually been able to take proper notes during EmacsConf itself was back in <a href="https://sachachua.com/blog/2013/04/emacs-conference-2013-sketchnotes-also-pdf/">2013</a>. It's okay, I get to spend a little time with presentations before anyone else does, and there's always the time afterwards. Plus I can always reach out to the speakers myself. It might be nice to be able to just sit and enjoy it and ask questions. Maybe someday when I've automated enough to make this something that I can run easily even on my own.
</p>

<p>
So let's quickly sketch out some possible scenarios:
</p>

<ul class="org-ul">
<li><b>I might need to do it on my own next year,</b> in case other organizers get pulled away at the last minute: I think it's possible, especially if I can plan for some emergency moderation or last-minute volunteers. I had a good experience, despite the stress of juggling things live on stream. (One time, one of the speakers had a question for me, and he had to repeat it a few times before I found the right tab and unmuted.) Still, I think it would be great to do it again next year.</li>
<li><b>Some of the other organizers might be more available:</b> It would be nice
to have people on screen handling the hosting. If people can help out with verifying the encoding, normalizing the audio, and shepherding videos through the process, that might let me free up some time to coordinate captions with other volunteers even for later submissions.</li>
<li><b>I might get better at asking for help and making it easier for people to get involved:</b> That would be pretty awesome. Sometimes it's hard for me to think about what and how, so if some people can take the initiative, that could be good.</li>
</ul>

<p>
This is good. It means that I can probably say yes to EmacsConf even if it's just me and whoever wants to share what they've been learning about Emacs this year. That's the basic level. We're still here, amazing! Anything else people can add to that is a welcome bonus. We'll make <a href="https://en.wikipedia.org/wiki/Stone_Soup">stone soup</a> together.
</p>

<p>
EmacsConf doesn't have to be snazzy. We don't need to try to out-market VS Code or whatever other editors emerge over the next 10 years. We can just keep having fun and doing awesome things with Emacs. I love the way that this online conference lets people participate from all over the world. We like to focus on facilitating sharing and then capturing the videos, questions, answers so that people can keep learning from them afterwards. I'm looking forward to more of that next year.
</p>

<p>
I'd love to hear what people thought of it and see how we can make things better together. I'd love to swap notes with organizers of other conferences, too. What's it like behind the scenes?
</p>

<p>
My next steps are:
</p>

<ul class="org-ul">
<li>Extract part of this into a report for the emacsconf.org website, like <a href="https://emacsconf.org/2024/report/">EmacsConf - 2024 - EmacsConf 2024 Report</a>.</li>
<li>Announce the resources and report on emacsconf-discuss.</li>
<li>Catch up on the other parts of my life I've been postponing.</li>
<li>Start thinking about EmacsConf 2026 =)</li>
</ul>
</div>
</div>
<div><a href="https://sachachua.com/blog/2026/01/emacsconf-2025-notes/index.org">View org source for this post</a></div>
<p>You can <a href="https://social.sachachua.com/@sacha/statuses/01KDZVMJ93REN4ZNJBMRQJE068" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F01%2Femacsconf-2025-notes%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 infrastructure upgrades</title>
		<link>https://sachachua.com/blog/2025/09/emacsconf-infrastructure-upgrades/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 23 Sep 2025 17:23:19 GMT</pubDate>
    <category>emacsconf</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/09/emacsconf-infrastructure-upgrades/</guid>
		<description><![CDATA[<p>
With the EmacsConf call for proposals now closed,
I have a little time before EmacsConf speakers
send in their pre-recorded videos come in for
captioning. I decided to dust off the
infrastructure to see what makes sense to upgrade.
</p>

<p>
We use <a href="https://etherpad.org/">Etherpad</a> for collaborative note-taking
during EmacsConf. It's straightforward to use and
pretty reliable. Conference participants can use
it to share notes, questions, and links. They can
also use IRC to ask questions, and volunteers copy
those questions into the pad for the talk. Hosts
and speakers can keep an eye on the pad for
questions. We send speakers a copy of their talk's
pad after the conference, and we post that along
with other follow-up questions on the conference
wiki. Here's an example: <a href="https://emacsconf.org/2024/talks/papers/">Writing academic papers
in Org-Roam</a>.
</p>

<p>
A native Emacs solution for collaborative notes
would be even better. <a href="https://elpa.gnu.org/packages/crdt.html">CRDT</a> was great for
experimenting with real-time collaboration within
Emacs, but I'm not sure it can handle a ton of
simultaneous connections and I don't want to find
out in the middle of the conference. Also, requiring
Emacs would leave out the people who only have a
web browser handy. It would be super cool if we
had something with Emacs, web, and IRC interfaces,
but for now, Etherpad will do.
</p>

<p>
We started by using <a href="https://etherpad.wikimedia.org/">Wikimedia's instance</a>, and then
we moved to hosting our own. For the past two
years, we've used Etherpad 1.9.7.
Etherpad is currently at version <a href="https://github.com/ether/etherpad-lite/releases/tag/v2.5.0">2.5.0</a>. There are
some performance improvements, bugfixes, and
security fixes, so I think it'll be worth
upgrading to that. I don't know of any specific
issues or upgrades, but it's a good idea to stay
closer to the latest release than to get too far
out of date.
</p>

<p>
I switched our
<a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/pad/tasks/main.yml">roles/pad/tasks/main.yml</a> to use
<a href="https://galaxy.ansible.com/ui/standalone/roles/systemli/etherpad/">systemli.etherpad</a> from ansible-galaxy. I also
figured out how to set up a <a href="https://developer.hashicorp.com/vagrant">Vagrant</a> virtual
machine that I could destroy and reconfigure with
<code>vagrant destroy; vagrant up &#45;&#45;provision</code>. Here's
my Vagrant file for that:
</p>


<div class="org-src-container">
<pre class="src src-ruby"><code><span class="org-comment-delimiter"># </span><span class="org-comment">-*- mode: ruby -*-</span>
<span class="org-type">Vagrant</span>.configure(<span class="org-string">"2"</span>) <span class="org-keyword">do</span> |config|
  config.vm.box = <span class="org-string">"debian/bookworm64"</span>
  config.vm.define <span class="org-string">"pad"</span> <span class="org-keyword">do</span> |pad|
  <span class="org-keyword">end</span>
  config.vm.provider <span class="org-string">"virtualbox"</span> <span class="org-keyword">do</span> |vb|
    vb.memory = <span class="org-string">"2048"</span>
  <span class="org-keyword">end</span>
  config.vm.network <span class="org-string">"private_network"</span>, <span class="org-constant">ip:</span> <span class="org-string">"192.168.56.2"</span>
  config.vm.provision <span class="org-string">"ansible"</span> <span class="org-keyword">do</span> |ansible|
    ansible.playbook = <span class="org-string">"../../vagrant-playbook.yml"</span>
  <span class="org-keyword">end</span>
<span class="org-keyword">end</span>
</code></pre>
</div>


<p>
The playbook it refers to has this:
</p>


<div class="org-src-container">
<pre class="src src-yaml"><code>- <span class="org-variable-name">name</span>: Pre-flight checks and package installation
  <span class="org-variable-name">hosts</span>: pad
  <span class="org-variable-name">become</span>: <span class="org-constant">true</span>
  <span class="org-variable-name">gather_facts</span>: <span class="org-constant">false</span>
  <span class="org-variable-name">pre_tasks</span>:
    - <span class="org-variable-name">name</span>: Ensure ntpdate is installed for time sync
      <span class="org-variable-name">ansible.builtin.apt</span>:
        <span class="org-variable-name">name</span>: ntpdate
        <span class="org-variable-name">state</span>: present
        <span class="org-variable-name">update_cache</span>: <span class="org-constant">yes</span>
    - <span class="org-variable-name">name</span>: Synchronize system clock
      <span class="org-variable-name">ansible.builtin.command</span>: ntpdate pool.ntp.org
      <span class="org-variable-name">changed_when</span>: <span class="org-constant">true</span>
    - <span class="org-variable-name">name</span>: Ensure ACL package is installed
      <span class="org-variable-name">ansible.builtin.apt</span>:
        <span class="org-variable-name">name</span>: acl
        <span class="org-variable-name">state</span>: present
- <span class="org-variable-name">name</span>: Load vars
  <span class="org-variable-name">hosts</span>: pad
  <span class="org-variable-name">tags</span>: always
  <span class="org-variable-name">tasks</span>:
    - <span class="org-variable-name">include_vars</span>:
        <span class="org-variable-name">file</span>: vagrant-vars.yml
- <span class="org-variable-name">name</span>: Set up pad proxy
  <span class="org-variable-name">hosts</span>: pad
  <span class="org-variable-name">tags</span>: proxy
  <span class="org-variable-name">roles</span>:
    - pad-proxy
- <span class="org-variable-name">name</span>: Set up pad
  <span class="org-variable-name">hosts</span>: pad
  <span class="org-variable-name">tags</span>: pad
  <span class="org-variable-name">roles</span>:
    - pad
</code></pre>
</div>


<p>
<a href="https://sachachua.com/blog/2013/12/setting-up-virtual-machines-with-vagrant/">I had used Vagrant in 2013</a>, and it felt good to
have the time to set up more testing
infrastructure. I liked being able to test my
<code>pad</code> and <code>pad-proxy</code> roles against a local
virtual machine. I could figure out whatever
tweaks I needed without messing up the production
instance that we use for some meetups in
between conferences.
</p>

<p>
Our production instance is on Debian 10 (Buster).
That has reached its end of life for security
updates, so <code>apt-cache update</code> doesn't work on it
any more, and those steps in my Ansible playbook
fail. I'm waiting for Amin Bandali to work on
upgrading that server, since he has other stuff
running on it. By setting <code>update_cache</code> variable
that I override in my inventory.yml and referring
to it with <code>update_cache: ""</code> in
my task, I can conditionally disable the apt-cache
update steps. That let me run the playbook against
the production server, and now we're on Etherpad 2.x.
</p>

<p>
I recently updated our <a href="https://bigbluebutton.org">BigBlueButton</a> instance to version 3.0.12. We've decided to stay with <a href="http://icecast.org/">Icecast</a> 2.4.4-1 for doing the livestreaming. We'll probably also keep <a href="https://obsproject.com">OBS</a> 29.1.2 and <a href="https://ffmpeg.org">ffmpeg</a> 6.0.1 instead of upgrading. With no must-have new features and other organizers' limited availability, it's better to keep those parts stable. This year, we'll continue using <a href="https://github.com/m-bain/whisperX">whisperx</a> to help with the first draft of captions, but we'll probably try <a href="https://huggingface.co/papers/2212.04356">large-v3</a> instead of <a href="https://huggingface.co/openai/whisper-large-v2">large-v2</a> by default. Some people find that large-v3's performance is better, some people find it's worse, so we'll see. Now that I know about whisperx's <code>&#45;&#45;initial_prompt</code> option, I might be able to nudge it to the vocabulary and punctuation style we like.
</p>

<p>
Since the bones seem pretty solid, I'm looking
forward to refamiliarizing myself with the Emacs
Lisp code I wrote to help run the conference. I
saved a bunch of improvement ideas from last year,
and I can't wait to turn them into code. That's
probably going to be lots of fun!</p>
<div><a href="https://sachachua.com/blog/2025/09/emacsconf-infrastructure-upgrades/index.org">View org source for this post</a></div>
<p>You can <a href="https://social.sachachua.com/@sacha/statuses/01K5VSNDEDB6HPJH31RTE9NN3H" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F09%2Femacsconf-infrastructure-upgrades%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>Scaling a BigBlueButton server down to a 1 GB node between uses</title>
		<link>https://sachachua.com/blog/2025/01/scaling-a-bigbluebutton-server-down-to-a-1-gb-node-between-uses/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 23 Jan 2025 19:53:47 GMT</pubDate>
    <category>geek</category>
<category>tech</category>
<category>emacsconf</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/01/scaling-a-bigbluebutton-server-down-to-a-1-gb-node-between-uses/</guid>
		<description><![CDATA[<p>
Now that we've survived <a href="https://emacsconf.org/">EmacsConf</a>, I've been
looking into running a <a href="https://bigbluebutton.org">BigBlueButton</a> server so
that various Emacs meetups can use it if they like
instead of relying on <a href="https://jitsi.org/">Jitsi</a> or other free
video-conferencing services. (I spent some time
looking into Galene, but I'm not quite sure that's
ready for our uses yet, like this <a href="https://github.com/jech/galene/issues/213">issue that
LibrePlanet ran into with recording</a>.)
</p>

<p>
BigBlueButton requires a server with at least 4
CPU cores and 8 GB of RAM to even start up, and it
doesn't like to share with other services. This
costs about USD 48+tax/month on <a href="https://www.linode.com/pricing/">Linode</a> or USD
576+tax/year, which is not an efficient use of
funds yet. I could delete it after each instance,
but I've been having a hard time properly
restoring it from backup after deploying to a new
IP address. <code>bbb-conf &#45;&#45;setip</code> doesn't seem to
catch everything, so I was still getting curl
errors related to verifying the certificate.
</p>

<p>
A reasonable in-between is to run it on Linode's
lowest plan (1 core, 1GB RAM; USD 60+tax for the
year) in between meetups, and then spin things up
for maybe 6-12 hours around each meetup. If I go
with the 4-core 8 GB setup, that would be an extra
USD 0.43 - 0.86 USD per meetup, which is eminently
doable. I could even go with the recommended
configuration of 8 cores and 16 GB memory on a
dedicated CPU plan (USD 0.216/hour, so USD 1.30 to
2.59 per meetup). This was the approach that we
used while preparing for EmacsConf. Since I didn't
have a lot of programming time, I scaled the node
up to 4 core / 8GB RAM whenever I had time to work
on it, and I scaled it down to 1GB at the end of
each of my working sessions. I scaled it up to
dedicated 8 core / 16 GB RAM for EmacsConf, during
which we used roughly half of the CPU capacity in
order to host a max of 107 simultaneous users over
7 meetings.
</p>

<p>
I reviewed my BigBlueButton setup notes in the
<a href="https://emacsconf.org/organizers-notebook/#bbb">EmacsConf organizers notebook</a> and <a href="https://emacsconf.org/2024/organizers-notebook/#check-emacsconf-infrastructure-bigbluebutton">the 2024
notebook</a> and set up a Linode instance under my
account, so that I can handle the billing and also
so that Amin Bandali doesn't get spammed by all
the notifications (up, down, up, down&#x2026;). And
then I'll be able to just scale it up when
EmacsConf comes around again, which is nice.
</p>

<p>
Anyway, BBB refuses to install on a machine with
fewer than 4 cores or 8 GB RAM, but once you set
it up, it'll valiantly thrash around even on an
underpowered server, which makes working with the
server over ssh a lot slower. Besides, that's not
friendly to other people using the same server. I
wanted to configure the services so that they
would only run on a server of the correct size. It
turns out that systemd will let you specify either
<code>ConditionMemory</code> and <code>ConditionCPUs</code> in the <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html">unit
configuration file</a>, and that you can use files
ending in <code>.conf</code> in a directory named like
<code>yourservicename.service.d</code> to override part of
the configuration. Clear examples were hard to
find, so I wanted to share these notes.
</p>

<p>
Since <code>ConditionMemory</code> is
specified in bytes (ex: 8000000000), I found
<code>ConditionCPUs</code> to be easier to read.
</p>

<p>
I used this command to check if I'd gotten the
syntax right:
</p>


<div class="org-src-container">
<pre class="src src-sh">systemd-analyze condition <span class="org-string">'ConditionCPUs=&gt;=4'</span>
</pre>
</div>


<p>
and then I wrote this script to set up the overrides:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">CPUS_REQUIRED</span>=4
<span class="org-keyword">for</span> ID<span class="org-keyword"> in</span> coturn.service redis-server.service bigbluebutton.target multi-user.target bbb-graphql-server.service bbb-rap-resque-worker.service bbb-webrtc-sfu.service bbb-fsesl-akka.service bbb-webrtc-recorder.service bbb-pads.service bbb-export-annotations.service bbb-web.service freeswitch.service etherpad.service bbb-rap-starter.service bbb-rap-caption-inbox.service freeswitch.service bbb-apps-akka.service bbb-graphql-actions postgresql@14-main.service; <span class="org-keyword">do</span>
    mkdir -p /etc/systemd/system/$<span class="org-variable-name">ID</span>.d
    printf <span class="org-string">"[Unit]\nConditionCPUs=&gt;=$CPUS_REQUIRED\n"</span> &gt; /etc/systemd/system/$<span class="org-variable-name">ID</span>.d/require-cpu.conf
<span class="org-keyword">done</span>
systemctl daemon-reload
systemd-analyze verify bigbluebutton.target
</pre>
</div>


<p>
It seems to work. When I use <a href="https://www.linode.com/products/cli/">linode-cli</a> to resize to the testing size, BigBlueButton works:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span>
<span class="org-builtin">source</span> /home/sacha/.profile
<span class="org-variable-name">PATH</span>=/home/sacha/.local/bin/:$<span class="org-variable-name">PATH</span>
linode-cli linodes resize $<span class="org-variable-name">BBB_ID</span> &#45;&#45;type g6-standard-4 &#45;&#45;allow_auto_disk_resize false
sleep 4m
linode-cli linodes boot $<span class="org-variable-name">BBB_ID</span>
sleep 3m
ssh root@bbb.emacsverse.org <span class="org-string">"bbb-conf &#45;&#45;restart; cd ~/greenlight-v3; docker compose restart"</span>
notify-send <span class="org-string">"Should be ready"</span>
</pre>
</div>


<p>
And when I resize it down to a 1 GB nanode,
BigBlueButton doesn't get started and the VPS is
nice and responsive when I SSH in.
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span>
<span class="org-builtin">source</span> /home/sacha/.profile
<span class="org-variable-name">PATH</span>=/home/sacha/.local/bin/:$<span class="org-variable-name">PATH</span>
<span class="org-builtin">echo</span> Powering off
linode-cli linodes shutdown $<span class="org-variable-name">BBB_ID</span>
sleep 60
<span class="org-builtin">echo</span> <span class="org-string">"Resizing BBB node to nanode, dormant"</span>
linode-cli linodes resize $<span class="org-variable-name">BBB_ID</span> &#45;&#45;type g6-nanode-1 &#45;&#45;allow_auto_disk_resize false
</pre>
</div>


<p>
So now I'm going to coordinate with Ihor Radchenko
about when he might want to try this out for
<a href="https://orgmode.org/worg/orgmeetup.html">OrgMeetup</a>, and I can talk to other meetup
organizers to figure out times. People will
probably want to test things before announcing it
to their meetup groups, so we just need to
schedule that. It's BigBlueButton 3.0. I'm not
100% confident in the setup. We had some technical
issues with some EmacsConf speakers even though we
did a tech check with them before we went live
with their session. Not sure what happened there.
</p>

<p>
I'm still a little nervous about accidentally
forgetting to downscale the server and running up
a bill, but I've scheduled downscaling with the <a href="https://www.geeksforgeeks.org/at-command-in-linux-with-examples/">at
command</a> before, so that's helpful. If it turns out
to be something we want to do regularly, I might
even be able to use a cronjob from my other server
so that it happens even if my laptop is off, and
maybe set up a backup nginx server with a friendly
message (and maybe a list of upcoming meetups) in
case people connect before it's been scaled up.
Anyway, I think that's a totally good use of part
of the <a href="https://sachachua.com/blog/2023/12/google-open-source-peer-bonus/">Google Open Source Peer Bonus</a> I received
last year.
</p>

<p>
As an aside, you can change a room's <code>friendly_id</code>
to something actually friendly. In the Rails
console (<code>docker exec -it greenlight-v3 bundle
exec rails console</code>), you could do something like
this:
</p>


<div class="org-src-container">
<pre class="src src-ruby"><span class="org-type">Room</span>.find_by(<span class="org-constant">friendly_id:</span> <span class="org-string">"CURRENT_ROOM_ID"</span>).update_attribute(<span class="org-constant">:friendly_id</span>, <span class="org-string">"NEW_CUSTOM_ID"</span>)
</pre>
</div>


<p>
Anyway, let me know if you organize an Emacs meetup and want to give this BigBlueButton instance a try!
</p>
<div><a href="https://sachachua.com/blog/2025/01/scaling-a-bigbluebutton-server-down-to-a-1-gb-node-between-uses/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%2Fscaling-a-bigbluebutton-server-down-to-a-1-gb-node-between-uses%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 2024 notes</title>
		<link>https://sachachua.com/blog/2024/12/emacsconf-2024-notes/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 28 Dec 2024 00:34:39 GMT</pubDate>
    <category>emacs</category>
<category>emacsconf</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/12/emacsconf-2024-notes/</guid>
		<description><![CDATA[<div class="update" id="orge2536cc">
<ul class="org-ul">
<li><span class="timestamp-wrapper"><span class="timestamp">[2025-01-10 Fri]</span></span>: <a href="https://news.ycombinator.com/item?id=42531217">Discussion on Hacker News</a> (<a href="https://ditzes.com/item/42531217">comments in chronological order</a>)</li>
<li><span class="timestamp-wrapper"><span class="timestamp">[2025-01-02 Thu]</span></span>: Add numbers based on latest Linode invoice.</li>
<li><span class="timestamp-wrapper"><span class="timestamp">[2024-12-28 Sat]</span></span>: Added talk and Q&amp;A count, added note about BBB max simultaneous users, added note about BBB, added thanks</li>
</ul>

</div>

<p>
<a href="https://emacsconf.org/2024/talks">The videos have been uploaded</a>, thank-you notes
have been sent, and the kiddo has decided to play
a little Minecraft on her own, so now I get to
write some quick notes on <a href="https://emacsconf.org/2024">EmacsConf 2024</a>.
</p>

<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-stats">Stats</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-timeline">Timeline</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-data">Data</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-communication">Communication</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-schedule">Schedule</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-recorded-videos">Recorded videos</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-recorded-introductions">Recorded introductions</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-captioning">Captioning</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-bigbluebutton-web-conference">BigBlueButton web conference</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-tech-checks-and-hosting">Tech checks and hosting</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-streaming">Streaming</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-publishing">Publishing</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-etherpad">Etherpad</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-irc">IRC</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-extracting-the-q-a">Extracting the Q&amp;A</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-budget-and-donations">Budget and donations</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-documentation-and-time">Documentation and time</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#orgf2abfa2">Thanks</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#emacsconf-2024-notes-overall">Overall</a></li>
</ul>
</div>
<div id="outline-container-emacsconf-2024-notes-stats" class="outline-2">
<h3 id="emacsconf-2024-notes-stats">Stats</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-stats">
<table>


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

<col class="org-right">
</colgroup>
<tbody>
<tr>
<td class="org-left">Talks</td>
<td class="org-right">31</td>
</tr>

<tr>
<td class="org-left">Hours</td>
<td class="org-right">10.7</td>
</tr>

<tr>
<td class="org-left">Q&amp;A web conferences</td>
<td class="org-right">21</td>
</tr>

<tr>
<td class="org-left">Hours</td>
<td class="org-right">7.8</td>
</tr>
</tbody>
</table>


<ul class="org-ul">
<li>Saturday:
<ul class="org-ul">
<li>gen: 177 peak + 14 peak lowres</li>
<li>dev: 226 peak + 79 peak lowres</li>
</ul></li>
<li>Sunday:
<ul class="org-ul">
<li>gen: 89 peak + 10 peak lowres</li>
</ul></li>
</ul>

<p>
Server configuration:
</p>

<table>


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

<col class="org-left">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left">meet</td>
<td class="org-left">16GB 8core dedicated</td>
<td class="org-left">peak 409% CPU (100% is 1 CPU), average 69.4%</td>
</tr>

<tr>
<td class="org-left">front</td>
<td class="org-left">32GB 8core shared</td>
<td class="org-left">peak 70.66% CPU (100% is 1 CPU)</td>
</tr>

<tr>
<td class="org-left">live</td>
<td class="org-left">64GB 16core shared</td>
<td class="org-left">peak 552% CPU (100% is 1 CPU) average 144%</td>
</tr>

<tr>
<td class="org-left">res</td>
<td class="org-left">46GB 12core</td>
<td class="org-left">peak 81.54% total CPU (100% is 12 CPUs); each OBS ~250%), mem 7GB used</td>
</tr>

<tr>
<td class="org-left">media</td>
<td class="org-left">3GB 1core</td>
<td class="org-left">&#xa0;</td>
</tr>
</tbody>
</table>

<p>
YouTube livestream stats:
</p>

<table>


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

<col class="org-right">

<col class="org-right">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Shift</th>
<th scope="col" class="org-right">Peak</th>
<th scope="col" class="org-right">Avg</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Gen Sat AM</td>
<td class="org-right">46</td>
<td class="org-right">28</td>
</tr>

<tr>
<td class="org-left">Gen Sat PM</td>
<td class="org-right">24</td>
<td class="org-right">16</td>
</tr>

<tr>
<td class="org-left">Dev Sat AM</td>
<td class="org-right">15</td>
<td class="org-right">7</td>
</tr>

<tr>
<td class="org-left">Dev Sat PM</td>
<td class="org-right">20</td>
<td class="org-right">12</td>
</tr>

<tr>
<td class="org-left">Gen Sun AM</td>
<td class="org-right">28</td>
<td class="org-right">17</td>
</tr>

<tr>
<td class="org-left">Gen Sun PM</td>
<td class="org-right">26</td>
<td class="org-right">18</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-timeline" class="outline-2">
<h3 id="emacsconf-2024-notes-timeline">Timeline</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-timeline">
<table>


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

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left">Call for proposals</td>
<td class="org-left"><span class="timestamp-wrapper"><span class="timestamp">[2024-06-30 Sun]</span></span></td>
</tr>

<tr>
<td class="org-left">CFP deadline</td>
<td class="org-left"><span class="timestamp-wrapper"><span class="timestamp">[2024-09-20 Fri]</span></span></td>
</tr>

<tr>
<td class="org-left">Speaker notifications</td>
<td class="org-left"><span class="timestamp-wrapper"><span class="timestamp">[2024-09-27 Fri]</span></span></td>
</tr>

<tr>
<td class="org-left">Publish schedule</td>
<td class="org-left"><span class="timestamp-wrapper"><span class="timestamp">[2024-10-25 Fri]</span></span></td>
</tr>

<tr>
<td class="org-left">Video target date</td>
<td class="org-left"><span class="timestamp-wrapper"><span class="timestamp">[2024-11-08 Fri]</span></span></td>
</tr>

<tr>
<td class="org-left">EmacsConf</td>
<td class="org-left"><span class="timestamp-wrapper"><span class="timestamp">[2024-12-07 Sat]</span></span>-<span class="timestamp-wrapper"><span class="timestamp">[2024-12-07 Sat]</span></span></td>
</tr>
</tbody>
</table>

<p>
We did early acceptances again this year. That was
nice. I wasn't sure about committing longer
periods of time early in the scheduling process,
so I usually tried to nudge people to plan a
20-minute video with the option of possibly doing
more, and I okayed longer talks once we figured
out what the schedule looked like.
</p>

<p>
There were 82 days between the call for proposals
and the CFP deadline, another 49 days from that to
the video target date, and 29 days between the
video target date and EmacsConf. It felt like
there was a good amount of time for proposals and
videos. Six videos came in before or on the target
date. The rest trickled in afterwards, which was
fine because we wanted to keep things low-pressure
for the speakers. We had enough capacity to
process and caption the videos as they came in.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-data" class="outline-2">
<h3 id="emacsconf-2024-notes-data">Data</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-data">
<p>
We continued to use an Org file to store the talk information.
It would be great to add some validation functions:
</p>

<ul class="org-ul">
<li>Check permissions and ownership for files</li>
<li>Check case sensitivity for Q&amp;A type detection</li>
<li>Check BBB redirect pages to make sure they exist</li>
<li>Check transcripts for ` because that messes up formatting;
consider escaping for the wiki</li>
<li>Check files are public and readable</li>
<li>Check captioned by comment vs caption status vs captioner</li>
</ul>

<p>
Speakers uploaded their files via <a href="https://github.com/psi-4ward/psitransfer">PsiTransfer</a>
again. I didn't get around to setting up the FTP
server. I should probably rename
ftp-upload.emacsconf.org to upload.emacsconf.org
so that people don't get confused.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-communication" class="outline-2">
<h3 id="emacsconf-2024-notes-communication">Communication</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-communication">
<p>
As usual, we announced the EmacsConf call for
proposals on <a href="https://lists.gnu.org/archive/html/emacs-tangents/2024-06/msg00004.html">emacs-tangents</a>, <a href="https://sachachua.com/blog/2024/07/2024-07-01-emacs-news/">Emacs News</a>,
<a href="https://lists.gnu.org/mailman/listinfo/emacsconf-discuss">emacsconf-discuss</a>, <a href="https://lists.gnu.org/archive/html/emacsconf-org/2024-06/msg00000.html">emacsconf-org</a>,
<a href="https://reddit.com/r/emacs">https://reddit.com/r/emacs</a>. <a href="https://systemcrafters.net/live-streams/july-12-2024/">System Crafters</a>,
<a href="https://irreal.org/blog/?p=12280">Irreal</a>, and <a href="https://emacs-apac.gitlab.io/announcements/november-2024/">Emacs APAC</a>, mentioned it, and people
also posted about EmacsConf on <a href="https://mastodon.social/tags/emacsconf">Mastodon</a>, <a href="https://x.com/search?q=%23emacsconf&amp;src=typed_query&amp;f=live">X</a>,
<a href="https://bsky.app/hashtag/emacsconf">BlueSky</a>, and <a href="https://www.facebook.com/story.php?story_fbid=538504738701826&amp;id=100076269125316&amp;_rdr">Facebook</a>. <a href="https://toot.si/@len/113392360015917614">@len@toot.si suggested</a>
submitting EmacsConf to <a href="https://foss.events">https://foss.events</a>, so I
did. There was some other <a href="https://www.reddit.com/r/emacs/comments/1h5c778/which_emacsconf_2024_talks_have_your_attention/">EmacsConf-related
discussions</a> in r/emacs. <a href="https://200ok.ch/posts/2024-09-16_announcing_emacsconf__official_swiss_satellite.html">200ok and Ardeo</a> organized
an in-person meetup in Switzerland, and <a href="https://dogodki.kompot.si/events/00a6f9ee-9087-400d-9d9b-d51b98561424">emacs.si got together in Ljubljana</a>.
</p>

<p>
For communicating with speakers and volunteers, I
used lots of mail merge
(<a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-mail.el">emacsconf-mail.el</a>). Most of the
templates only needed a little tweaking from last
year's code. I added a function to help me
double-check delivery, since the batches that I
tried to send via async sometimes ran into errors.
</p>

<p>
Next time, I think it could be interesting to add
more blog posts and Mastodon toots.
</p>

<p>
Also, maybe it would be good to get in touch with podcasts like
</p>

<ul class="org-ul">
<li><a href="https://systemcrafters.net/">System Crafters</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLbFVcOQ-YH_LRP687N0YeN78YZmBp5wqF">This Week in Linux</a></li>
<li><a href="https://linuxunplugged.com/">Linux Unplugged</a></li>
<li><a href="http://asknoahshow.com/">Ask Noah</a></li>
<li><a href="https://linuxafterdark.net/">Linux After Dark</a></li>
<li><a href="https://anonradio.net/">Lispy Gopher Show</a></li>
</ul>

<p>
to give a heads up on EmacsConf before it
happens and also let them know when videos are
available.
</p>

<p>
We continued to use <a href="https://www.mumble.info/">Mumble</a> for backstage coordination. It worked out well.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-schedule" class="outline-2">
<h3 id="emacsconf-2024-notes-schedule">Schedule</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-schedule">
<p>
The schedule worked out to two days of talks, with
two tracks on the first day, and about 15-20
minutes between each talk. We were able to adapt
to late submissions, last-minute cancellations,
and last-minute switches from Q&amp;A to live.
</p>

<p>
We added an open mic session on Sunday to fill in
the time from a last-minute cancellation. That
worked out nicely and it might be a good idea to
schedule in that time next year. It was also good
to move some of the usual closing remarks earlier.
We were able to wrap up in a timely manner, which
was great for some hosts and participants because
they didn't have to stay up so late.
</p>

<p>
Sunday was single-track, so it was nice and
relaxed. I was a little worried that people might
get bored if the current talk wasn't relevant to
their interests, but everyone managed just fine. I
probably should have remembered that Emacs people
are good at turning extra time into more
configuration tweaks.
</p>

<p>
Most of the scheduling was determined by people's
time constraints, so I didn't worry too much about
making the talks flow logically. I accidentally
forgot to note down one speaker's time
constraints, but he caught it when we e-mailed the
draft schedule and I was able to move things
around for a better time for him.
</p>

<p>
There was a tiny bit of technical confusion
because the automated schedule publishing on res
had case-sensitive matching (<code>case-fold-search</code>
was set to <code>nil</code>), so if a talk was set to "Live"
Q&amp;A, it didn't announce it as a live talk because
it was looking for <code>live</code>. Whoops. I've added that
configuration setting to my
<code>emacsconf-stream-config.el</code>, so the ansible
scripts should get it next time.
</p>

<p>
I asked Leo and Corwin if they wanted to manually
control the talks this year. They opted to leave
it automatically managed by crontab so that they
wouldn't have to worry as much about timekeeping.
It worked reliably. Hooray for automation! The
only scheduling hiccup was because I turned off
the crontab so that we could do Saturday closing
remarks when we wanted to and I forgot to reenable
autopilot the next day. We noticed when the
opening remarks didn't start right on the dot, and
I got everything back on track.
</p>

<p>
Like last year, I scheduled the dev track to start
a little later than the gen track. That made for a
less frantic morning. Also, this year we scheduled
Sunday morning to start with more IRC Q&amp;A instead
of live Q&amp;A. We didn't notice any bandwidth issues
on Sunday morning this time.
</p>

<p>
It would be nice to have Javascript countdowns in
some kind of web interface to make it easier for
hosts, especially if we can update it with the
actual time the current video will end in MPV.
</p>

<p>
I can also update the <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-stream.el">emacsconf-stream.el</a> code to
make it easier to automatically count down to the
next talk or to a specific talk.
</p>

<p>
We have Javascript showing local time on the
individual talk pages, but it would be nice to
localize the times on all the schedule/watch pages
too.
</p>

<p>
Most of my stuff (scheduling, publishing, etc.) is
handled by automation with just a little bit of
manual nudging every so often, so it might be
possible to organize an event that's more friendly
to Europe/APAC timezones.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-recorded-videos" class="outline-2">
<h3 id="emacsconf-2024-notes-recorded-videos">Recorded videos</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-recorded-videos">
<p>
As usual, we strongly encouraged speakers to
record videos to lower everyone's stress levels
and allow for captioning by volunteers, so that's
what most speakers did. We were able to handle
a few last-minute submissions as well as a
live talk. Getting videos also meant we could
publish them as each talk went live, including
automatically putting the videos and transcripts
on the wiki.
</p>

<p>
We didn't have obvious video encoding cut-offs, so
re-encoding in a screen was a reliable way to
avoid interruptions this year. Also, no one
complained about tiny text or low resolution, so
the talk preparation instructions seem to be
working out.
</p>

<p>
Automatically normalizing the audio with
ffmpeg-normalize didn't work out, so Leo Vivier
did a last-minute scramble to normalize the audio
the day before the conference. Maybe that's
something that volunteers can help with during the
lead-up to the conference, or maybe I can finally
figure out how to fit that into my process. I
don't have much time or patience to listen to
things, but it would be nice to get that sorted
out early.
</p>

<p>
Next year we can try remixing the audio to mono.
One of the talks had some audio moving around,
which was a little distracting. Also, some people
listen to the talks in one ear, so it would be
good to drop things down to mono for them.
</p>

<p>
We think 60fps videos stressed the res server a
bit, resulting in dropped frames. Next year, we
can downsample those to 30fps and add a note to
the talk preparation instructions. The hosts also
suggested looking into setting up streaming from
each host's computer instead of using our shared
VNC sessions.
</p>

<p>
There was some colour smearing and weirdness when
we played some videos with mpv on res. Upgrading
MPV to v0.38 fixed it.
</p>

<p>
Some people requested dark mode (light text on
dark background), so maybe we can experiment with
recommending that next year.
</p>

<p>
I did a last-minute change to the shell scripts to
load resources from the cache directory instead of
the assets/stream directory, but I didn't get all
of the file references, so sometimes the test
videos played or the introductions didn't have
captions. On the plus side, I learned how to use
<code>j</code> in MPV to reload a subtitle file.
</p>

<p>
Sometimes we needed to play the videos manually.
If we get the hang of starting MPV in a screen or
tmux session, it might be easier for hosts to
check how much time is left, or to restart a video
at a specific point if needed. Leo said he'll work
on figuring out the configuration and the Lua
scripts.
</p>

<p>
I uploaded all the videos to YouTube and scheduled
them. That was nice because then I didn't have to
keep updating things during the conference. It
turns out that Toobnix also has a way to schedule
uploads. I just need to upload it as unlisted
first, and then choose Scheduled from the
visibility. I wonder if <a href="https://www.npmjs.com/package/%40peertube%2Fpeertube-cli">peertube-cli</a> can be
extended to schedule things. Anyway, since I
didn't know about that during the conference, I
just used <code>emacsconf-publish-upload-talk</code> function
to upload videos.
</p>

<p>
It was fun playing <a href="https://www.youtube.com/watch?v=urcL86UpqZc">Interview with an Emacs
Enthusiast in 2023 [Colorized] - YouTube</a> at lunch.
I put together some captions for it after the
conference, so maybe we can play it with captions
next year.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-recorded-introductions" class="outline-2">
<h3 id="emacsconf-2024-notes-recorded-introductions">Recorded introductions</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-recorded-introductions">
<p>
We record introductions so that hosts don't have
to worry about how to say things on air. I should
probably send the intro check e-mail
earlier&#x2013;maybe on the original video target date,
even if speakers haven't submitted their videos
yet. This will reduce the last-minute scramble to
correct intros.
</p>

<p>
When I switched the shell scripts to use the cache
directory, I forgot to get it to do the intros
from that directory as well, so some of the
uncorrected intros were played.
</p>

<p>
I forgot to copy the intro VTTs to the cache
directory. This should be handled by the
subed-record process for creating intros, so it'll
be all sorted out next year.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-captioning" class="outline-2">
<h3 id="emacsconf-2024-notes-captioning">Captioning</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-captioning">
<p>
We used <a href="https://github.com/m-bain/whisperX">WhisperX</a> for speech-to-text this year. It
did a great job at preparing the first drafts of
captions that our wonderful army of volunteer
captioners could then edit. WhisperX's built-in
voice activity detection cut down a lot on the
hallucinations that <a href="https://github.com/openai/whisper">OpenAI Whisper</a> had during
periods of silence in last year's captions, and
there was only one instance of WhisperX missing a
chunk of text from a speaker that I needed to
manually fill in. I upgraded to a Lenovo P52 with
64GB RAM, so I was able to handle last-minute
caption processing on my computer. It might be
handy to have a smaller model ready for those
last-minute requests, or have something ready to
go for the commercial APIs.
</p>

<p>
The timestamps were a little bit off. It was
really helpful that speakers and volunteers used
the backstage area to check video quality. I used
<a href="https://www.readbeyond.it/aeneas/">Aeneas</a> to re-align the text, but Aeneas was also
confused by silences. I've added some code to
<a href="https://github.com/sachac/subed">subed</a> so that I can realign regions of subtitles
using Aeneas or WhisperX timestamps, and I also
wrote some code to <a href="https://sachachua.com/blog/2024/11/checking-caption-timing-by-skimming-with-emacs-lisp-or-js/">skim timestamps for easy
verification</a>.
</p>

<p>
Anush V experimented with using machine learning
for <a href="https://gitlab.com/jun8git/sub-seg">subtitle segmentation</a>, so that might be
something to explore going forward.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-bigbluebutton-web-conference" class="outline-2">
<h3 id="emacsconf-2024-notes-bigbluebutton-web-conference">BigBlueButton web conference</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-bigbluebutton-web-conference">
<p>
This year we set up a new <a href="https://demo.bigbluebutton.org/">BigBlueButton</a> web conferencing server. The server with our previous BigBlueButton instance had been donated by a defunct nonprofit, so it finally got removed on October 27. After investigating whether Jitsi or Galene might be a good fit for EmacsConf, we decided to continue with BigBlueButton. There were some concerns about <a href="https://github.com/bigbluebutton/bbb-install/issues/261">non-free Mongo</a> for BBB versions &gt;= 2.3 and &lt; 3, so I installed BBB 3.0. This was hard to get working on a Docker on the existing res server. <a href="https://emacsconf.org/2024/organizers-notebook/#bbb">We decided</a> it was worth spinning up an additional Linode virtual private server. It turned out that BBB refused to run on anything smaller than 8GB/4core, so I scaled up to that during testing, scaled back down to 1GB/1core in between, and scaled up to 16GB/8core dedicated during the conference.
</p>

<p>
I'm still not 100% sure I set everything up
correctly or that everything was stable. Maybe
next year BBB 3.0 will be better-tested, someone
more sysad-y can doublecheck the setup, or we can
try <a href="https://galene.org/">Galene</a>.
</p>

<p>
One of the benefits of upgrading to BBB 3.0 was
that we could use the smart layout feature to drag
the webcam thumbnails to the side of the shared
screen. This made shared screens much easier to
read. I haven't automated this yet, but it was
easy enough for us to do via the shared VNC
session.
</p>

<p>
On the plus side, it was pretty straightforward to use the Rails console to create all the rooms. We used moderator access codes to give all the speakers moderator access. Mysteriously, superadmins didn't automatically have moderator access to all the rooms even if they were logged in, so we needed to add host access by hand so that they could start the recordings.
</p>

<p>
Since we self-hosted and were budgeting more for the full-scale node, I didn't feel comfortable scaling it up to production size until a few days before the conference. I sent the access codes with the check-in e-mails to give speakers time to try things out.
</p>

<p>
<a href="https://sachachua.com/blog/2023/12/emacsconf-backstage-figuring-out-our-maximum-number-of-simultaneous-bigbluebutton-users/">Compared to last year's stats</a>:
</p>

<table>


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

<col class="org-right">

<col class="org-right">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">&#xa0;</th>
<th scope="col" class="org-right">2023</th>
<th scope="col" class="org-right">2024</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Max number of simultaneous users</td>
<td class="org-right">62</td>
<td class="org-right">107</td>
</tr>

<tr>
<td class="org-left">Max number of simultaneous meetings</td>
<td class="org-right">6</td>
<td class="org-right">7</td>
</tr>

<tr>
<td class="org-left">Max number of people in one meeting</td>
<td class="org-right">27</td>
<td class="org-right">25</td>
</tr>

<tr>
<td class="org-left">Total unique people</td>
<td class="org-right">84</td>
<td class="org-right">102</td>
</tr>

<tr>
<td class="org-left">Total unique talking</td>
<td class="org-right">36</td>
<td class="org-right">40</td>
</tr>
</tbody>
</table>

<p>
(Max number of simultaneous users wasn't deduplicated, since we need that number for server load planning)
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-tech-checks-and-hosting" class="outline-2">
<h3 id="emacsconf-2024-notes-tech-checks-and-hosting">Tech checks and hosting</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-tech-checks-and-hosting">
<p>
FlowyCoder did a great job getting everyone
checked in, especially once I figured out the
right checklist to use. We used people's emergency
contact information a couple of times.
</p>

<p>
Corwin and Leo were able to jump in and out of the
different streams for hosting. Sometimes they were
both in the same Q&amp;A session, which made it more
conversational especially when they were covering
for technical issues. We had a couple of crashes
even though the tech checks went fine, so that was
weird. Maybe something's up with BBB 3.0 or how I
set it up.
</p>

<p>
Next time, we can consider asking speakers what
kind of facilitation style they like. A chatty
host? Someone who focuses on reading the questions
and then gets out of the way? Speakers reading
their own questions and the host focusing on
timekeeping/troubleshooting?
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-streaming" class="outline-2">
<h3 id="emacsconf-2024-notes-streaming">Streaming</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-streaming">
<p>
I experimented with setting up the live0 streaming
node as a 64GB 32core dedicated CPU server, but
that was overkill, so we went back down to 64GB
16core and it still didn't approach the CPU
limits.
</p>

<p>
The 480p stream seemed stable, hooray! I had set
it up last year to automatically kick in as soon
as I started streaming to Icecast, and that worked
out. I think I changed a loop to be <code>while true</code>
instead of making it try 5 times, so that probably
helped.
</p>

<p>
I couldn't get Toobnix livestreaming to work this
year. On the plus side, that meant that I could
use OBS to directly stream to YouTube instead of
trying to set up multicasting. I set up one
YouTube livestreaming event for each shift and
added the RTMP keys to our shift checklists so
that I could update the settings before starting
the stream. That was pretty straightforward.
</p>

<p>
This year, I wrote a little randomizer function to
display things on the countdown screen. At first I
just dumped in
<a href="https://www.gnu.org/fun/jokes/gnuemacs.acro.exp.en.html">https://www.gnu.org/fun/jokes/gnuemacs.acro.exp.en.html</a>,
but some of those were not quite what I was
looking for. (&#x2026; Probably should've read them all
first!) Then I added random packages from GNU ELPA
and NonGNU ELPA, and that was more fun. I might
add MELPA next time too. The code for dumping
random packages is probably worth putting into a
different blog post, since it's the sort of thing
people might like to add to their dashboards or
screensavers.
</p>

<p>
I ran into some C-s annoyances in screen even with
flow control turned off, so it might be a good
idea to switch to tmux instead of screen.
</p>

<p>
Next year, I think it might be a good idea to make
intro images for each talk. Then we can use that
as the opening slide in BigBlueButton (unless
they're already sharing something else) as well as
a video thumbnail.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-publishing" class="outline-2">
<h3 id="emacsconf-2024-notes-publishing">Publishing</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-publishing">
<p>
The automated process for publishing talks and
transcripts to the wiki occasionally needed
nudging when someone else had committed a change
to the wiki. I thought I had a <code>git pull</code> in there
somewhere, but maybe I need to look at it some
more.
</p>

<p>
I forgot to switch the conference publishing phase
and enable the inclusion of Etherpads, but
fortunately Ihor noticed. I did some last-minute
hacking to add them in, and then I remembered the
variables I needed to set. Just need to add it to
our process documentation.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-etherpad" class="outline-2">
<h3 id="emacsconf-2024-notes-etherpad">Etherpad</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-etherpad">
<p>
We used <a href="https://etherpad.org/">Etherpad</a> 1.9.7 to collect Q&amp;A again this
year. I didn't upgrade to Etherpad v2.x because I
couldn't figure out how to get it running within
the time I set aside for it, but maybe that's
something for next year.
</p>

<p>
I wrote some Elisp to copy the current ERC line
(unwrapped) for easier pasting into Etherpad. That
worked out really well, and it let me keep up with
copying questions from IRC to the pad in between
other bits of running around.
(<code>emacsconf-erc-copy</code> in
<a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-erc.el">emacsconf-erc.el</a>)
</p>

<p>
Next year, I'll add pronouns and pronunciations to
the Etherpad template so that hosts can refer to
them easily.
</p>

<p>
If I rejig the template to move the next/previous
links so that notes can be added to the end, I
might be able to use the Etherpad API to add text
from IRC.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-irc" class="outline-2">
<h3 id="emacsconf-2024-notes-irc">IRC</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-irc">
<p>
We remembered to give the libera.chat people a
heads-up before the conference, so we didn't run
into usage limits for <a href="https://chat.emacsconf.org">https://chat.emacsconf.org</a>. Yay!
</p>

<p>
Aside from writing <code>emacsconf-erc-copy</code>
(<a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-erc.el">emacsconf-erc.el</a>) to make it easier
to add text from IRC to the Etherpad, I didn't
tinker much with the IRC setup for this year. It
continued to be a solid platform for discussion.
</p>

<p>
I think a keyboard shortcut for inserting a talk's
URL could be handy and should be pretty easy to
add to my Embark keymap.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-extracting-the-q-a" class="outline-2">
<h3 id="emacsconf-2024-notes-extracting-the-q-a">Extracting the Q&amp;A</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-extracting-the-q-a">
<p>
We sometimes forgot to start the recording for the
Q&amp;A until a few minutes into the talk. I
considered extracting the Q&amp;A recordings from the
Icecast dump or YouTube stream recordings in order
to get those first few minutes, but decided it
wasn't worth it since people could generally
figure out the answers.
</p>

<p>
Getting the recordings off BigBlueButton was
easier this year because I configured it with
<a href="https://docs.bigbluebutton.org/3.0/administration/customize/#install-additional-recording-processing-formats">video as an additional processing format</a>, so we
could grab one file per session instead of
combining the different streams with ffmpeg.
</p>

<p>
I did a quick pass of the Q&amp;A transcripts and chat
logs to see if people mentioned anything that they
might want to take out. I also copied IRC messages
and the pads, and I copied over the answers from
the transcripts using the new
<code>emacsconf-extract-subed-copy-section-text</code>
function.
</p>

<p>
Audio mixing was uneven. It might be nice to
figure out separate audio recordings just in case
(<a href="https://github.com/bigbluebutton/bigbluebutton/issues/12302">#12302</a>, <a href="https://groups.google.com/g/bigbluebutton-dev/c/oPzJy0mKOrw">bigbluebutton-dev</a>). We ended up not
tinkering with the audio for the Q&amp;A, so next
time, I can probably upload them without waiting
to see if anyone wants to fiddle with the audio.
</p>

<p>
Trimming the Q&amp;A was pretty straightforward. I
added a <code>subed-crop-media-file</code> function to <a href="https://github.com/sachac/subed">subed</a>
so that I can trim files easily.
</p>

<p>
Thanks to my completion functions for adding
section headings based on comments, it was easy to
index the Q&amp;A this year. I didn't even put it up
backstage for people to work on.
</p>

<p>
Nudged by <a href="https://mastodon.social/@ctietze/113627423761735937">@ctietze</a>, I'm experimenting with adding
sticky videos if Javascript is enabled so that
it's easier to navigate using the transcript.
There's still a bit of tinkering to do, but
it's a start.
</p>

<p>
I added some conference-related variables to a
<code>.dir-locals.el</code> file so that I can more easily
update things even for past conferences. This is
mostly related to publishing the captions on the
wiki pages, which I do with Emacs Lisp.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-budget-and-donations" class="outline-2">
<h3 id="emacsconf-2024-notes-budget-and-donations">Budget and donations</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-budget-and-donations">
<p>
Costs (USD, not including 13% tax):
</p>

<table>


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

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-right">52.54</td>
<td class="org-left">Extra costs for hosting in December</td>
</tr>

<tr>
<td class="org-right">3.11</td>
<td class="org-left">Extra costs for BBB testing in November</td>
</tr>

<tr>
<td class="org-right">120</td>
<td class="org-left">Hosting costs year-round (two Linode nanodes)</td>
</tr>
</tbody>
</table>

<p>
Total of USD 175.65 + tax, or USD 198.48 for 2024.
</p>

<p>
The Free Software Foundation also provided
<a href="https://media.emacsconf.org">media.emacsconf.org</a> for serving media files. Ry P
provided res.emacsconf.org for OBS streaming over
VNC sessions.
</p>

<p>
Amin Bandali was away during the conference
weekend and no one else knew how to get the list
of donors and current donation stats from the FSF
Working Together program on short notice. Next
time, we can get that sorted out beforehand so
that we can thank donors properly.
</p>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-documentation-and-time" class="outline-2">
<h3 id="emacsconf-2024-notes-documentation-and-time">Documentation and time</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-documentation-and-time">
<p>
I think my biggest challenge was having less time
to prepare for EmacsConf this year because the
kiddo wanted more of my attention. In many ways,
the automation that I'd been gradually building up
paid off. We were able to pull together EmacsConf
even though I had limited focus time.
</p>

<p>
Here's my Emacs-related time data (including Emacs
News and tweaking my config):
</p>

<table>


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

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">

<col class="org-right">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Year</th>
<th scope="col" class="org-right">Jan</th>
<th scope="col" class="org-right">Feb</th>
<th scope="col" class="org-right">March</th>
<th scope="col" class="org-right">April</th>
<th scope="col" class="org-right">May</th>
<th scope="col" class="org-right">June</th>
<th scope="col" class="org-right">July</th>
<th scope="col" class="org-right">Aug</th>
<th scope="col" class="org-right">Sept</th>
<th scope="col" class="org-right">Oct</th>
<th scope="col" class="org-right">Nov</th>
<th scope="col" class="org-right">Dec</th>
<th scope="col" class="org-right">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">2023</td>
<td class="org-right">23.4</td>
<td class="org-right">15.9</td>
<td class="org-right">16.2</td>
<td class="org-right">11.2</td>
<td class="org-right">4.4</td>
<td class="org-right">11.5</td>
<td class="org-right">6.5</td>
<td class="org-right">13.3</td>
<td class="org-right">36.6</td>
<td class="org-right">86.6</td>
<td class="org-right">93.2</td>
<td class="org-right">113.0</td>
<td class="org-right">432</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">71.2</td>
<td class="org-right">12.0</td>
<td class="org-right">5.6</td>
<td class="org-right">6.6</td>
<td class="org-right">3.3</td>
<td class="org-right">9.6</td>
<td class="org-right">11.0</td>
<td class="org-right">4.7</td>
<td class="org-right">36.0</td>
<td class="org-right">40.3</td>
<td class="org-right">52.3</td>
<td class="org-right">67.7</td>
<td class="org-right">320</td>
</tr>
</tbody>
</table>

<p>
(and here's a <a href="https://sachachua.com/blog/2023/12/analyzing-my-emacs-time-over-the-last-11-years-or-so/">longer-term analysis going back to 2012</a>.)
</p>

<p>
I spent 92.6 hours total in October and November
2024 doing Emacs-related things, compared to 179.8
hours the previous year &#x2013; so, around half the
time. Part of the 2023 total was related to
preparing my presentation for EmacsConf, so I was
much more familiar with my scripts then.
Apparently, there was still a lot more that I
needed to document. As I scrambled to get
EmacsConf sorted out, I captured quick tasks/notes
for the things I need to add to our organizers
notebook. Now I get to go through all those notes
in my inbox. Maybe next year will be even
smoother.
</p>

<p>
On the plus side, all the process-related
improvements meant that the other volunteers could
jump in pretty much whenever they wanted,
including during the conference itself. I didn't
want to impose firm commitments on people or bug
them too much by e-mail, so we kept things very
chill in terms of scheduling and planning. If
people were available, we had stuff people could
help with. If people were busy, that was fine, we
could manage. This was nice, especially when I
applied the same sort of chill approach to myself.
</p>

<p>
I'd like to eventually get to the point of being
able to mostly follow my checklists and notes from
the start of the conference planning process to
the end. I've been moving notes from year-specific
organizer notebooks to the main <a href="https://emacsconf.org/organizers-notebook/">organizers'
notebook</a>. I plan to keep that one as the main file
for notes and processes, and then to have specific
dates and notes in the yearly ones.
</p>
</div>
</div>
<div id="outline-container-orgf2abfa2" class="outline-2">
<h3 id="orgf2abfa2">Thanks</h3>
<div class="outline-text-2" id="text-orgf2abfa2">
<ul class="org-ul">
<li>Thank you to all the speakers, volunteers, and participants, and to all those other people in our lives who make it possible through time and support.</li>
<li>Thanks to Leo Vivier and Corwin Brust for hosting the sessions, and to FlowyCoder for checking people in.</li>
<li>Thanks to our proposal review volunteers James Howell, JC Helary, and others for helping with the early acceptance process.</li>
<li>Thanks to our captioning volunteers: Mark Lewin, Rodrigo Morales, Anush, annona, and James Howell, and some speakers who captioned their own talks.</li>
<li>Thanks to Leo Vivier for fiddling with the audio to get things nicely synced.</li>
<li>Thanks to volunteers who kept the mailing lists free from spam.</li>
<li>Thanks to Bhavin Gandhi, Christopher Howard, Joseph Turner, and screwlisp for quality-checking.</li>
<li>Thanks to shoshin for the music.</li>
<li>Thanks to Amin Bandali for help with infrastructure and communication.</li>
<li>Thanks to Ry P for the server that we're using for OBS streaming and for processing videos.</li>
<li>Thanks to the Free Software Foundation for Emacs itself, the mailing lists, the media.emacsconf.org server, and handling donations on our behalf through the FSF Working Together program. <a href="https://www.fsf.org/working-together/fund">https://www.fsf.org/working-together/fund</a></li>
<li>Thanks to the many users and contributers and project teams that create all the awesome free software we use, especially: BigBlueButton, Etherpad, Icecast, OBS, TheLounge, libera.chat, ffmpeg, OpenAI Whisper, WhisperX, the aeneas forced alignment tool, PsiTransfer, subed, and many, many other tools and services we used to prepare and host this years conference</li>
<li>Thanks to everyone!</li>
</ul>
</div>
</div>
<div id="outline-container-emacsconf-2024-notes-overall" class="outline-2">
<h3 id="emacsconf-2024-notes-overall">Overall</h3>
<div class="outline-text-2" id="text-emacsconf-2024-notes-overall">
<p>
Good experience. Lots of fun. I'd love to do it
again next year. EmacsConf feels like a nice, cozy
get-together where people share the cool things
they've been working on and thinking about. People had fun!
They said:
</p>

<ul class="org-ul">
<li>"emacsconf is absolutely knocking it out of the park when it comes to conference logistics"</li>
<li>"I think this conference has defined the terms for a successful online conference."</li>
<li>"EmacsConf is one of the big highlights of my year every year. Thank you a ton for running this 😊"</li>
</ul>

<p>
It's one of the highlights of my year too. =) Looking forward to the next one!
</p>

<p>
In the meantime, y'all can stay connected via <a href="https://sachachua.com/blog/category/emacs-news/">Emacs News</a>, <a href="https://emacs-berlin.org/">meetups (online and in person)</a>, <a href="https://planet.emacslife.com/">Planet Emacslife</a>, and now <a href="https://emacs.tv/">emacs.tv</a>. Enjoy!
</p>

<p>
p.s. I'd love to learn from other people's conference blog posts, EmacsConf or otherwise. I'm particularly interested in virtual conferences and how we can tinker with them to make them even better. I'm having a hard time finding posts; please feel free to send me links to ones you've liked or written!
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/12/emacsconf-2024-notes/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%2F12%2Femacsconf-2024-notes%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: Makefile targets</title>
		<link>https://sachachua.com/blog/2024/11/emacsconf-backstage-makefile-targets/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Mon, 11 Nov 2024 00:21:04 GMT</pubDate>
    <category>emacsconf</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/11/emacsconf-backstage-makefile-targets/</guid>
		<description><![CDATA[<div class="update" id="orgf79c41f">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2024-11-16 Sat]</span></span>: Removed <code>highlight_words</code> from whisperx call.
</p>

</div>

<p>
We like to use pre-recorded videos at EmacsConf to
minimize technical risks. This also means we can
caption them beforehand, stream them with open
captions, and publish them as soon as the talk
goes live.
</p>

<p>
Here's the process:
</p>

<ol class="org-ol">
<li>Speakers upload their videos in whatever format they like. We use <a href="https://github.com/psi-4ward/psitransfer">PsiTransfer</a> to accept the uploaded files.</li>
<li>We rename the files to have the talk title and speaker name in the filename, like <code>emacsconf-2024-emacs30&#45;&#45;emacs-30-highlights&#45;&#45;philip-kaludercic&#45;&#45;original.mov</code>.</li>
<li>We use <a href="https://www.ffmpeg.org/">FFmpeg</a> to reencode them to WEBM so that everything is available in a free format, and we replace the <code>&#45;&#45;original.*</code> part with <code>&#45;&#45;reencoded.webm</code>. We copy this to <code>&#45;&#45;main.webm</code> as a starting point.</li>
<li>We extract the audio and save it to <code>&#45;&#45;reencoded.opus</code>.</li>
<li>We use <a href="https://github.com/slhck/ffmpeg-normalize">ffmpeg-normalize</a> to normalize the audio and save it to <code>&#45;&#45;normalized.opus</code>.</li>
<li>We use <a href="https://github.com/m-bain/whisperX">WhisperX</a> to get a reasonable starting point for captions, which we save to <code>&#45;&#45;reencoded.vtt</code>. I <a href="https://sachachua.com/dotemacs#remove-whisperx-underline">remove the underlines</a> and the tsv and srt files.</li>
<li>Someone edits the captions. We save edited captions as <code>&#45;&#45;main.vtt</code>.</li>
<li><code>&#45;&#45;normalized.opus</code> and <code>&#45;&#45;main.vtt</code> get combined into <code>&#45;&#45;main.webm</code>.</li>
</ol>

<p>
I've been slowly learning how to set up Makefile
rules to automate more and more of this. Let's go
through parts of the
<a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/prerec/templates/Makefile">roles/prerec/templates/Makefile</a>.
</p>

<div class="sticky-toc" id="org2a83a08">
<nav id="table-of-contents" role="doc-toc">
<h3>Table of Contents</h3>
<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/index.xml#reencoded">Make the reencoded webm from the original MP4, MOV, MKV, or WEBM</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#audio">Process the audio and captions</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#combine">Combine the video, audio, and subtitles</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#targets">Making all the files based on the original ones that are available</a></li>
</ul>
</div>
</nav>

</div>
<div id="outline-container-reencoded" class="outline-2">
<h3 id="reencoded">Make the reencoded webm from the original MP4, MOV, MKV, or WEBM</h3>
<div class="outline-text-2" id="text-reencoded">
<p>
Here's the rule that makes a <code>&#45;&#45;reencoded.webm</code> based on the original mp4, mov, mkv, or webm.
</p>


<div class="org-src-container">
<pre class="src src-makefile"><span class="org-variable-name">VIDEO_EXTS</span> = mp4 mkv webm mov
<span class="org-variable-name">source_patterns</span> = $(<span class="org-variable-name">foreach</span> ext,$(<span class="org-variable-name">VIDEO_EXTS</span>),$(<span class="org-variable-name">1</span>)&#45;&#45;original.$(<span class="org-variable-name">ext</span>))
<span class="org-makefile-targets">emacsconf-%&#45;&#45;reencoded.webm</span>: <span class="org-variable-name">SOURCES</span> = $(<span class="org-variable-name">call</span> source_patterns, emacsconf-<span class="org-makefile-targets">$</span><span class="org-makefile-targets"><span class="org-constant">*</span></span>)
<span class="org-makefile-targets">emacsconf-%&#45;&#45;reencoded.webm</span>:
  $(<span class="org-variable-name">eval</span> SOURCE := $(<span class="org-variable-name">lastword</span> $(<span class="org-variable-name">sort</span> $(<span class="org-variable-name">wildcard</span> $(<span class="org-variable-name">SOURCES</span>)))))
  <span class="org-type">@</span><span class="org-makefile-shell">if [ -z </span><span class="org-string"><span class="org-makefile-shell">"$(</span></span><span class="org-string"><span class="org-makefile-shell"><span class="org-variable-name">SOURCE</span></span></span><span class="org-string"><span class="org-makefile-shell">)"</span></span><span class="org-makefile-shell"> ]; then \</span>
    echo <span class="org-string">"No source file found for </span><span class="org-makefile-targets"><span class="org-string">$</span></span><span class="org-makefile-targets"><span class="org-string"><span class="org-constant">@</span></span></span><span class="org-string">"</span>; \
    echo <span class="org-string">"Tried: $(</span><span class="org-string"><span class="org-variable-name">SOURCES</span></span><span class="org-string">)"</span>; \
    exit 1; \
  fi
  <span class="org-type">@</span><span class="org-makefile-shell">echo </span><span class="org-string"><span class="org-makefile-shell">"Using source: $(</span></span><span class="org-string"><span class="org-makefile-shell"><span class="org-variable-name">SOURCE</span></span></span><span class="org-string"><span class="org-makefile-shell">)"</span></span>
  ./reencode-in-screen.sh <span class="org-string">"$(</span><span class="org-string"><span class="org-variable-name">SOURCE</span></span><span class="org-string">)"</span>
</pre>
</div>


<p>
Reencoding can take a while and it's prone to me
accidentally breaking it, so we stick it in a GNU
screen so that I don't accidentally quit it. This is <code>reencode-in-screen.sh</code>:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span>
<span class="org-variable-name">ORIGINAL</span>=$<span class="org-variable-name">1</span>
<span class="org-variable-name">BASE</span>=<span class="org-string">"${ORIGINAL%&#45;&#45;original.*}"</span>
<span class="org-variable-name">REENCODED</span>=<span class="org-string">"${BASE}&#45;&#45;reencoded.webm"</span>
<span class="org-variable-name">SLUG</span>=$(<span class="org-sh-quoted-exec">echo "$ORIGINAL" | perl -ne '/^emacsconf-[0-9]*-(.*?</span><span class="org-string">)&#45;&#45;/ &amp;&amp; print $1'</span>)
<span class="org-variable-name">LOCK</span>=<span class="org-string">".lock-$SLUG"</span>

<span class="org-keyword">if</span> [ <span class="org-negation-char">!</span> -f <span class="org-string">"$REENCODED"</span> ]; <span class="org-keyword">then</span>
    <span class="org-keyword">if</span> [  -f <span class="org-string">"$LOCK"</span> ]; <span class="org-keyword">then</span>
        <span class="org-builtin">echo</span> <span class="org-string">"$LOCK already exists, waiting for it"</span>
    <span class="org-keyword">else</span>
        touch <span class="org-string">"$LOCK"</span>
        screen -dmS reencode-$<span class="org-variable-name">SLUG</span> /bin/bash -c <span class="org-string">"reencode.sh \"$ORIGINAL\" \"$REENCODED\" &amp;&amp; thumbnail.sh \"$MAIN\" &amp;&amp; rm \"$LOCK\""</span>
        <span class="org-builtin">echo</span> <span class="org-string">"Processing $REENCODED in reencode-$SLUG"</span>
    <span class="org-keyword">fi</span>
<span class="org-keyword">fi</span>
</pre>
</div>


<p>
which calls
<a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/prerec/templates/reencode.sh">roles/prerec/templates/reencode.sh</a>.
Here's the templatized version from Ansible:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/usr/bin/</span><span class="org-keyword">env</span><span class="org-comment"> bash</span>

<span class="org-builtin">set</span> -euo pipefail

<span class="org-comment-delimiter"># </span><span class="org-comment">Defaults</span>
<span class="org-variable-name">q</span>={{ reencode_quality }}
<span class="org-variable-name">cpu</span>={{ reencode_cpu }}
<span class="org-variable-name">time_limit</span>=<span class="org-string">""</span>
<span class="org-variable-name">print_only</span>=false
<span class="org-variable-name">limit_resolution</span>={{ res_y }}
<span class="org-variable-name">limit_fps</span>={{ fps }}

<span class="org-keyword">while </span><span class="org-builtin">getopts</span> :q:c:t:s OPT; <span class="org-keyword">do</span>
    <span class="org-keyword">case</span> $<span class="org-variable-name">OPT</span><span class="org-keyword"> in</span>
        q|+q)
            <span class="org-variable-name">q</span>=<span class="org-string">"$OPTARG"</span>
            ;;
        c|+c)
            <span class="org-variable-name">cpu</span>=<span class="org-string">"$OPTARG"</span>
            ;;
        t|+t)
            <span class="org-variable-name">time_limit</span>=<span class="org-string">"-to $OPTARG"</span>
            ;;
        s)
            <span class="org-variable-name">print_only</span>=true
            ;;
        *)
            <span class="org-builtin">echo</span> <span class="org-string">"usage: `basename $0` [+-q ARG] [+-c ARG} [&#45;&#45;] ARGS..."</span>
            <span class="org-keyword">exit</span> 2
    <span class="org-keyword">esac</span>
<span class="org-keyword">done</span>
<span class="org-builtin">shift</span> <span class="org-sh-quoted-exec">`expr $OPTIND - 1`</span>
<span class="org-variable-name">OPTIND</span>=1

<span class="org-variable-name">input</span>=<span class="org-string">"$1"</span>
<span class="org-variable-name">output</span>=<span class="org-string">"${2:-$(</span><span class="org-sh-quoted-exec">echo $input | sed 's/&#45;&#45;original.*/&#45;&#45;reencoded.webm/'</span><span class="org-string">)}"</span>

<span class="org-variable-name">command</span>=<span class="org-string">"$(</span><span class="org-sh-quoted-exec">cat&lt;&lt;EOF</span>
<span class="org-string">ffmpeg -y -i "$input" $time_limit \</span>
<span class="org-string">       -vf "scale='-1':'min($limit_resolution,ih)',</span>
<span class="org-string">            fps='$limit_fps'" \</span>
<span class="org-string">       -c:v libvpx-vp9 -b:v 0 -crf $q -an \</span>
<span class="org-string">       -row-mt 1 -tile-columns 2 -tile-rows 2 -cpu-used $cpu -g 240 \</span>
<span class="org-string">       -pass 1 -f webm -threads $cpu /dev/null &amp;&amp;</span>
<span class="org-string">    ffmpeg -y -i "$input" $time_limit \</span>
<span class="org-string">           -vf "scale='-1':'min($limit_resolution,ih)',</span>
<span class="org-string">                fps='$limit_fps'" \</span>
<span class="org-string">               -c:v libvpx-vp9 -b:v 0 -crf $q -c:a libopus \</span>
<span class="org-string">               -row-mt 1 -tile-columns 2 -tile-rows 2 -cpu-used $cpu \</span>
<span class="org-string">               -pass 2 -threads $cpu &#45;&#45; "$output"</span>
<span class="org-string">EOF</span>
<span class="org-string">)"</span>

<span class="org-keyword">if</span> [ $<span class="org-variable-name">print_only</span> == true ]; <span class="org-keyword">then</span>
    <span class="org-builtin">echo</span> <span class="org-string">"$command"</span>
<span class="org-keyword">else</span>
    <span class="org-builtin">eval</span> <span class="org-string">"$command"</span>
<span class="org-keyword">fi</span>
</pre>
</div>

</div>
</div>
<div id="outline-container-audio" class="outline-2">
<h3 id="audio">Process the audio and captions</h3>
<div class="outline-text-2" id="text-audio">
<p>
Processing the audio is relatively straightforward.
</p>


<div class="org-src-container">
<pre class="src src-makefile"><span class="org-makefile-targets">emacsconf-%&#45;&#45;reencoded.opus</span>: emacsconf-%&#45;&#45;reencoded.webm
  <span class="org-makefile-targets">ffmpeg -i </span><span class="org-string"><span class="org-makefile-targets">"$</span></span><span class="org-string"><span class="org-constant"><span class="org-makefile-targets">&lt;</span></span></span><span class="org-string"><span class="org-makefile-targets">"</span></span><span class="org-makefile-targets"> -c</span>:a copy <span class="org-string">"</span><span class="org-makefile-targets"><span class="org-string">$</span></span><span class="org-makefile-targets"><span class="org-string"><span class="org-constant">@</span></span></span><span class="org-string">"</span>

<span class="org-makefile-targets">emacsconf-%&#45;&#45;normalized.opus</span>: emacsconf-%&#45;&#45;reencoded.opus
  <span class="org-makefile-targets">ffmpeg-normalize </span><span class="org-string"><span class="org-makefile-targets">"$</span></span><span class="org-string"><span class="org-constant"><span class="org-makefile-targets">&lt;</span></span></span><span class="org-string"><span class="org-makefile-targets">"</span></span><span class="org-makefile-targets"> -ofmt opus -c</span>:a libopus -o <span class="org-string">"</span><span class="org-makefile-targets"><span class="org-string">$</span></span><span class="org-makefile-targets"><span class="org-string"><span class="org-constant">@</span></span></span><span class="org-string">"</span>

<span class="org-makefile-targets">emacsconf-%&#45;&#45;reencoded.vtt</span>: emacsconf-%&#45;&#45;reencoded.opus
  whisperx &#45;&#45;model large-v2 &#45;&#45;align_model WAV2VEC2_ASR_LARGE_LV60K_960H &#45;&#45;compute_type int8 &#45;&#45;print_progress True &#45;&#45;max_line_width 50 &#45;&#45;segment_resolution chunk &#45;&#45;max_line_count 1 &#45;&#45;language en <span class="org-string">"$</span><span class="org-string"><span class="org-constant">&lt;</span></span><span class="org-string">"</span>
</pre>
</div>


<p>
After this, we need to manually process the
<code>&#45;&#45;reencoded.vtt</code> and then eventually save the
edited version as <code>&#45;&#45;main.vtt</code>.
</p>
</div>
</div>
<div id="outline-container-combine" class="outline-2">
<h3 id="combine">Combine the video, audio, and subtitles</h3>
<div class="outline-text-2" id="text-combine">
<p>
The next part of the Makefile creates the <code>&#45;&#45;main.webm</code> from the reencoded, normalized, and edited files, or from just the <code>&#45;&#45;reencoded.webm</code> if that's all that's available.
</p>


<div class="org-src-container">
<pre class="src src-makefile"><span class="org-makefile-targets">emacsconf-%&#45;&#45;main.webm</span>: emacsconf-%&#45;&#45;reencoded.webm emacsconf-%&#45;&#45;normalized.opus emacsconf-%&#45;&#45;main.vtt
  ffmpeg -i emacsconf-<span class="org-makefile-targets">$</span><span class="org-makefile-targets"><span class="org-constant">*</span></span>&#45;&#45;reencoded.webm -i emacsconf-<span class="org-makefile-targets">$</span><span class="org-makefile-targets"><span class="org-constant">*</span></span>&#45;&#45;normalized.opus -i emacsconf-<span class="org-makefile-targets">$</span><span class="org-makefile-targets"><span class="org-constant">*</span></span>&#45;&#45;main.vtt \
    -map 0:v -map 1:a -c:v copy -c:a copy \
    -map 2 -c:s webvtt -y \
    <span class="org-makefile-targets">$</span><span class="org-makefile-targets"><span class="org-constant">@</span></span>

<span class="org-makefile-targets">emacsconf-%&#45;&#45;main.webm</span>: emacsconf-%&#45;&#45;reencoded.webm
  cp <span class="org-string">"$</span><span class="org-string"><span class="org-constant">&lt;</span></span><span class="org-string">"</span> <span class="org-string">"</span><span class="org-makefile-targets"><span class="org-string">$</span></span><span class="org-makefile-targets"><span class="org-string"><span class="org-constant">@</span></span></span><span class="org-string">"</span>
</pre>
</div>


<p>
This works because the Makefile picks the most specific set of dependencies.
</p>
</div>
</div>
<div id="outline-container-targets" class="outline-2">
<h3 id="targets">Making all the files based on the original ones that are available</h3>
<div class="outline-text-2" id="text-targets">
<p>
Finally, we need some rules to make various
things. We do this with a wildcard match for all
the original files, and then we make a list
without the <code>&#45;&#45;original.*</code>. After that, we can
just use <code>addsuffix</code> to add the different file
endings.
</p>


<div class="org-src-container">
<pre class="src src-makefile"><span class="org-variable-name">PRERECS_ORIGINAL</span> := $(<span class="org-variable-name">wildcard</span> emacsconf-*&#45;&#45;original.*)
<span class="org-variable-name">PREFIXES</span> := $(<span class="org-variable-name">shell</span> for f in $(<span class="org-variable-name">PRERECS_ORIGINAL</span>); do echo <span class="org-string">"$${f%&#45;&#45;original.*}"</span>; done)
<span class="org-variable-name">PRERECS_REENCODED</span> := $(<span class="org-variable-name">addsuffix</span> &#45;&#45;reencoded.webm, $(<span class="org-variable-name">PREFIXES</span>))
<span class="org-variable-name">PRERECS_OPUS</span> := $(<span class="org-variable-name">addsuffix</span> &#45;&#45;reencoded.opus, $(<span class="org-variable-name">PREFIXES</span>))
<span class="org-variable-name">PRERECS_NORMAL</span> := $(<span class="org-variable-name">addsuffix</span> &#45;&#45;normalized.opus, $(<span class="org-variable-name">PREFIXES</span>))
<span class="org-variable-name">PRERECS_MAIN</span> := $(<span class="org-variable-name">addsuffix</span> &#45;&#45;main.webm, $(<span class="org-variable-name">PREFIXES</span>))
<span class="org-variable-name">PRERECS_CAPTIONS</span> := $(<span class="org-variable-name">addsuffix</span> &#45;&#45;reencoded.vtt, $(<span class="org-variable-name">PREFIXES</span>))

<span class="org-makefile-targets">all</span>: reencoded opus normal main
<span class="org-makefile-targets">reencoded</span>: $(<span class="org-variable-name">PRERECS_REENCODED</span>)
<span class="org-makefile-targets">opus</span>: $(<span class="org-variable-name">PRERECS_OPUS</span>)
<span class="org-makefile-targets">normal</span>: $(<span class="org-variable-name">PRERECS_NORMAL</span>)
<span class="org-makefile-targets">captions</span>: $(<span class="org-variable-name">PRERECS_CAPTIONS</span>)
<span class="org-makefile-targets">main</span>: $(<span class="org-variable-name">PRERECS_MAIN</span>)
</pre>
</div>


<p>
I sometimes do the captions on my computer, so
I've left them out of the <code>all</code> target.
</p>

<p>
Seems to be doing all right so far. It's nice
having the Makefile figure out what's changed and
what needs to be updated.
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/11/emacsconf-backstage-makefile-targets/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%2F11%2Femacsconf-backstage-makefile-targets%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 lots of intro videos with subed-record</title>
		<link>https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 05 Jan 2024 13:02:09 GMT</pubDate>
    <category>emacsconf</category>
<category>subed</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/</guid>
		<description><![CDATA[<summary id="orgccd1ac4">
<p>
Summary (735 words): Emacs is a handy audio/video editor. subed-record
can combine multiple audio files and images to create multiple output
videos.
</p>
</summary>

<p>
</p><div><iframe width="456" height="315" title="YouTube" video="" player"="" src="https://www.youtube-nocookie.com/embed/QskeNbGbMa4" frameborder="0" allowfullscreen=""></iframe><a href="https://www.youtube.com/watch?v=QskeNbGbMa4">Watch on YouTube</a></div>
<p></p>

<p>
It's nice to feel like you're saying someone's name correctly. We ask
EmacsConf speakers to introduce themselves in the first few seconds of
their video, but people often forget to do that, so that's okay. We
started recording introductions for EmacsConf 2022 so that stream
hosts don't have to worry about figuring out pronunciation while
they're live. Here's how I used <a href="https://github.com/sachac/subed-record">subed-record</a> to turn my recordings
into lots of little videos.
</p>

<p>
First, I generated the title images by using Emacs Lisp to replace
text in a template SVG and then using Inkscape to convert the SVG into
a PNG. Each image showed information for the previous talk as well as
the upcoming talk. (<a href="https://sachachua.com/blog/feed/index.xml#emacsconf-intro-images"><code>emacsconf-stream-generate-in-between-pages</code></a>)
</p>


<figure id="org7862343">
<img src="https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/emacsconf.svg.png" alt="emacsconf.svg.png">

<figcaption><span class="figure-number">Figure 1: </span>Sample title image</figcaption>
</figure>

<p>
Then I generated the text for each talk based on the title, the
speaker names, pronunciation notes, pronouns, and type of Q&amp;A. Each
introduction generally followed the pattern, "Next we have <i>title</i> by
<i>speakers</i>. <i>Details about Q&amp;A</i>." (<a href="https://sachachua.com/blog/feed/index.xml#emacsconf-intro-script"><code>emacsconf-pad-expand-intro</code> and
<code>emacsconf-subed-intro-subtitles</code> below</a>)
</p>

<pre class="example" style="white-space: break-spaces" id="orge024b6b">
00:00:00.000 &#45;&#45;&gt; 00:00:00.999
#+OUTPUT: sat-open.webm
[[file:/home/sacha/proj/emacsconf/2023/assets/in-between/sat-open.svg.png]]
Next, we have "Saturday opening remarks".

00:00:05.000 &#45;&#45;&gt; 00:00:04.999
#+OUTPUT: adventure.webm
[[file:/home/sacha/proj/emacsconf/2023/assets/in-between/adventure.svg.png]]
Next, we have "An Org-Mode based text adventure game for learning the basics of Emacs, inside Emacs, written in Emacs Lisp", by Chung-hong Chan. He will answer questions via Etherpad.
</pre>

<p>
I copied the text into an Org note in my inbox, which Syncthing copied
over to the Orgzly Revived app on my Android phone. I used Google
Recorder to record the audio. I exported the m4a audio file and a
rough transcript, copied them back via Syncthing, and <a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/">used
subed-record to edit the audio into a clean audio file without
oopses</a>.
</p>

<p>
Each intro had a set of captions that started with a <code>NOTE</code> comment.
The <code>NOTE</code> comment specified the following:
</p>
<ul class="org-ul">
<li><code>#+AUDIO:</code>: the audio source to use for the timestamped captions
that follow</li>
<li><code>[[file:...]]</code>: the title image I generated for each talk. When
<code>subed-record-compile-video</code> sees a comment with a link to an image,
video, or animated GIF, it takes that visual and uses it for the
span of time until the next visual.</li>
<li><code>#+OUTPUT:</code> the file to create.</li>
</ul>


<div class="org-src-container">
<pre class="src src-subed-vtt"><span class="org-comment">NOTE #+OUTPUT: hyperdrive.webm</span>
<span class="org-comment">[[file:/home/sacha/proj/emacsconf/2023/assets/in-between/hyperdrive.svg.png]]</span>
<span class="org-comment">#+AUDIO: intros-2023-11-21-cleaned.opus</span>

<span class="org-subed-time">00:00:15.680</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:17.599</span>
Next, we have "hyperdrive.el:

<span class="org-subed-time">00:00:17.600</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:21.879</span>
Peer-to-peer filesystem in Emacs", by Joseph Turner

<span class="org-subed-time">00:00:21.880</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:25.279</span>
and Protesilaos Stavrou (also known as Prot).

<span class="org-subed-time">00:00:25.280</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:27.979</span>
Joseph will answer questions via BigBlueButton,

<span class="org-subed-time">00:00:27.980</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:31.080</span>
and Prot might be able to join depending on the weather.

<span class="org-subed-time">00:00:31.081</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:33.439</span>
You can join using the URL from the talk page

<span class="org-subed-time">00:00:33.440</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:36.320</span>
or ask questions through Etherpad or IRC.

<span class="org-comment">NOTE</span>
<span class="org-comment">#+OUTPUT: steno.webm</span>
<span class="org-comment">[[file:/home/sacha/proj/emacsconf/2023/assets/in-between/steno.svg.png]]</span>
<span class="org-comment">#+AUDIO: intros-2023-11-19-cleaned.opus</span>

<span class="org-subed-time">00:03:23.260</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:03:25.480</span>
Next, we have "Programming with steno",

<span class="org-subed-time">00:03:25.481</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:03:27.700</span>
by Daniel Alejandro Tapia.

<span class="org-comment">NOTE</span>
<span class="org-comment">#+AUDIO: intro-2023-11-29-cleaned.opus</span>

<span class="org-subed-time">00:00:13.620</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:16.580</span>
You can ask your questions via Etherpad and IRC.

<span class="org-subed-time">00:00:16.581</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:18.079</span>
We'll send them to the speaker

<span class="org-subed-time">00:00:18.080</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:19.919</span>
and post the answers in the talk page

<span class="org-subed-time">00:00:19.920</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:21.320</span>
after the conference.
</pre>
</div>


<p>
I could then call <code>subed-record-compile-video</code> to create the videos
for all the intros, or mark a region with <code>C-SPC</code> and then
<code>subed-record-compile-video</code> only the intros inside that region.
</p>

<p>
<a href="https://media.emacsconf.org/2023/emacsconf-2023-emacsconf&#45;&#45;emacsconforg-how-we-use-org-mode-and-tramp-to-organize-and-run-a-multitrack-conference&#45;&#45;sacha-chua&#45;&#45;intro.webm">Sample intro</a>
</p>

<p>
Using Emacs to edit the audio and compile videos worked out really
well because it made it easy to change things.
</p>

<ul class="org-ul">
<li><b>Changing pronunciation or titles:</b> For EmacsConf 2023, I got the
recordings sorted out in time for the speakers to correct my
pronunciation if they wanted to. Some speakers also changed their
talk titles midway. If I wanted to redo an intro, I just had to
rerecord that part, run it through my subed-record audio cleaning
process, add an <code>#+AUDIO:</code> comment specifying which file I want to
take the audio from, paste it into my main <code>intros.vtt</code>, and
recompile the video.</li>

<li><b>Cancelling talks:</b> One of the talks got cancelled, so I needed to
update the images for the talk before it and the talk after it. I
regenerated the title images and recompiled the videos. I didn't
even need to figure out which talk needed to be updated - it was easy
enough to just recompile all of them.</li>

<li><b>Changing type of Q&amp;A:</b> For example, some speakers needed to switch
from answering questions live to answering them after the
conference. I could just delete the old instructions, paste in the
instructions from elsewhere in my <code>intros.vtt</code> (making sure to set
<code>#+AUDIO</code> to the file if it came from a different take), and
recompile the video.</li>
</ul>

<p>
And of course, all the videos were captioned. Bonus!
</p>

<p>
So that's how using Emacs to edit and compile simple videos saved me a
lot of time. I don't know how I'd handle this otherwise. 47 video
projects that might all need to be updated if, say, I changed the
template? Yikes. Much better to work with text. Here are the technical
details.
</p>
<div id="outline-container-emacsconf-intro-images" class="outline-2">
<h3 id="emacsconf-intro-images">Generating the title images</h3>
<div class="outline-text-2" id="text-emacsconf-intro-images">
<p>
I used Inkscape to add IDs to our template SVG so that I could edit
them with Emacs Lisp. From <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-stream.el">emacsconf-stream.el</a>:
</p>

<p>
</p><details open=""><summary>emacsconf-stream-generate-in-between-pages: Generate the title images.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-generate-in-between-pages</span> (<span class="org-type">&amp;optional</span> info)
  <span class="org-doc">"Generate the title images."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">setq</span> info (<span class="org-keyword">or</span> emacsconf-schedule-draft (emacsconf-publish-prepare-for-display (emacsconf-filter-talks (<span class="org-keyword">or</span> info (emacsconf-get-talk-info))))))
  (<span class="org-keyword">let*</span> ((by-track (seq-group-by (<span class="org-keyword">lambda</span> (o) (plist-get o <span class="org-builtin">:track</span>)) info))
         (dir (expand-file-name <span class="org-string">"in-between"</span> emacsconf-stream-asset-dir))
         (template (expand-file-name <span class="org-string">"template.svg"</span> dir)))
    (<span class="org-keyword">unless</span> (file-directory-p dir)
      (make-directory dir t))
    (mapc (<span class="org-keyword">lambda</span> (track)
            (<span class="org-keyword">let</span> (prev)
              (mapc (<span class="org-keyword">lambda</span> (talk)
                      (<span class="org-keyword">let</span> ((dom (xml-parse-file template)))
                        (mapc (<span class="org-keyword">lambda</span> (entry)
                                (<span class="org-keyword">let</span> ((prefix (car entry)))
                                  (emacsconf-stream-svg-set-text dom (concat prefix <span class="org-string">"title"</span>)
                                                 (plist-get (cdr entry) <span class="org-builtin">:title</span>))
                                  (emacsconf-stream-svg-set-text dom (concat prefix <span class="org-string">"speakers"</span>)
                                                 (plist-get (cdr entry) <span class="org-builtin">:speakers</span>))
                                  (emacsconf-stream-svg-set-text dom (concat prefix <span class="org-string">"url"</span>)
                                                 (<span class="org-keyword">and</span> (cdr entry) (concat emacsconf-base-url (plist-get (cdr entry) <span class="org-builtin">:url</span>))))
                                  (emacsconf-stream-svg-set-text
                                   dom
                                   (concat prefix <span class="org-string">"qa"</span>)
                                   (<span class="org-keyword">pcase</span> (plist-get (cdr entry) <span class="org-builtin">:q-and-a</span>)
                                     ((<span class="org-keyword">rx</span> <span class="org-string">"live"</span>) <span class="org-string">"Live Q&amp;A after talk"</span>)
                                     ((<span class="org-keyword">rx</span> <span class="org-string">"pad"</span>) <span class="org-string">"Etherpad"</span>)
                                     ((<span class="org-keyword">rx</span> <span class="org-string">"IRC"</span>) <span class="org-string">"IRC Q&amp;A after talk"</span>)
                                     (_ <span class="org-string">""</span>)))))
                              (list (cons <span class="org-string">"previous-"</span> prev)
                                    (cons <span class="org-string">"current-"</span> talk)))
                        (<span class="org-keyword">with-temp-file</span> (expand-file-name (concat (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">".svg"</span>) dir)
                          (dom-print dom))
                        (shell-command
                         (concat <span class="org-string">"inkscape &#45;&#45;export-type=png -w 1280 -h 720 &#45;&#45;export-background-opacity=0 "</span>
                                 (shell-quote-argument (expand-file-name (concat (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">".svg"</span>)
                                                                         dir)))))
                      (<span class="org-keyword">setq</span> prev talk))
                    (emacsconf-filter-talks (cdr track)))))
          by-track)))
</pre></div></details>
<p></p>

<p>
</p><details><summary>emacsconf-stream-svg-set-text: Update DOM to set the tspan in the element with ID to TEXT.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-svg-set-text</span> (dom id text)
  <span class="org-doc">"Update DOM to set the tspan in the element with ID to TEXT.</span>
<span class="org-doc">If the element doesn't have a tspan child, use the element itself."</span>
  (<span class="org-keyword">if</span> (<span class="org-keyword">or</span> (null text) (string= text <span class="org-string">""</span>))
      (<span class="org-keyword">let</span> ((node (dom-by-id dom id)))
        (<span class="org-keyword">when</span> node
          (dom-set-attribute node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span> <span class="org-string">"visibility: hidden"</span>)
          (dom-set-attribute (dom-child-by-tag node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">tspan</span>) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span> <span class="org-string">"fill: none; stroke: none"</span>)))
    (<span class="org-keyword">setq</span> text (svg&#45;&#45;encode-text text))
    (<span class="org-keyword">let</span> ((node (<span class="org-keyword">or</span> (dom-child-by-tag
                     (car (dom-by-id dom id))
                     <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">tspan</span>)
                    (dom-by-id dom id))))
      (<span class="org-keyword">cond</span>
       ((null node)
        (<span class="org-warning">error</span> <span class="org-string">"Could not find node %s"</span> id))                      <span class="org-comment-delimiter">; </span><span class="org-comment">skip</span>
       ((= (length node) 2)
        (nconc node (list text)))
       (t (<span class="org-keyword">setf</span> (elt node 2) text))))))
</pre></div></details>
<p></p>
</div>
</div>
<div id="outline-container-emacsconf-intro-script" class="outline-2">
<h3 id="emacsconf-intro-script">Generating the script</h3>
<div class="outline-text-2" id="text-emacsconf-intro-script">
<p>
From <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-pad.el">emacsconf-pad.el</a>:
</p>

<p>
</p><details open=""><summary>emacsconf-pad-expand-intro: Make an intro for TALK.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-pad-expand-intro</span> (talk)
  <span class="org-doc">"Make an intro for TALK."</span>
  (<span class="org-keyword">cond</span>
   ((null (plist-get talk <span class="org-builtin">:speakers</span>))
    (format <span class="org-string">"Next, we have \"%s\"."</span> (plist-get talk <span class="org-builtin">:title</span>)))
   ((plist-get talk <span class="org-builtin">:intro-note</span>)
    (plist-get talk <span class="org-builtin">:intro-note</span>))
   (t
    (<span class="org-keyword">let</span> ((pronoun (<span class="org-keyword">pcase</span> (plist-get talk <span class="org-builtin">:pronouns</span>)
                     ((<span class="org-keyword">rx</span> <span class="org-string">"she"</span>) <span class="org-string">"She"</span>)
                     ((<span class="org-keyword">rx</span> <span class="org-string">"\"ou\""</span> <span class="org-string">"Ou"</span>))
                     ((<span class="org-keyword">or</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">nil</span> <span class="org-string">"nil"</span> (<span class="org-keyword">rx</span> string-start <span class="org-string">"he"</span>) (<span class="org-keyword">rx</span> <span class="org-string">"him"</span>)) <span class="org-string">"He"</span>)
                     ((<span class="org-keyword">rx</span> <span class="org-string">"they"</span>) <span class="org-string">"They"</span>)
                     (_ (<span class="org-keyword">or</span> (plist-get talk <span class="org-builtin">:pronouns</span>) <span class="org-string">""</span>)))))
      (format <span class="org-string">"Next, we have \"%s\", by %s%s.%s"</span>
              (plist-get talk <span class="org-builtin">:title</span>)
              (replace-regexp-in-string <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-negation-char">^</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">", and \\1"</span>
                                        (plist-get talk <span class="org-builtin">:speakers</span>))
              (emacsconf-surround <span class="org-string">" ("</span> (plist-get talk <span class="org-builtin">:pronunciation</span>) <span class="org-string">")"</span> <span class="org-string">""</span>)
              (<span class="org-keyword">pcase</span> (plist-get talk <span class="org-builtin">:q-and-a</span>)
                ((<span class="org-keyword">or</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">nil</span> <span class="org-string">""</span>) <span class="org-string">""</span>)
                ((<span class="org-keyword">rx</span> <span class="org-string">"after"</span>) <span class="org-string">" You can ask questions via Etherpad and IRC. We'll send them to the speaker, and we'll post the answers on the talk page afterwards."</span>)
                ((<span class="org-keyword">rx</span> <span class="org-string">"live"</span>)
                 (format <span class="org-string">" %s will answer questions via BigBlueButton. You can join using the URL from the talk page or ask questions through Etherpad or IRC."</span>
                         pronoun
                         ))
                ((<span class="org-keyword">rx</span> <span class="org-string">"pad"</span>)
                 (format <span class="org-string">" %s will answer questions via Etherpad."</span>
                         pronoun
                         ))
                ((<span class="org-keyword">rx</span> <span class="org-string">"IRC"</span>)
                 (format <span class="org-string">" %s will answer questions via IRC in the #%s channel."</span>
                         pronoun
                         (plist-get talk <span class="org-builtin">:channel</span>)))))))))
</pre></div></details>
<p></p>

<p>
And from <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-subed.el">emacsconf-subed.el</a>:
</p>

<p>
</p><details open=""><summary>emacsconf-subed-intro-subtitles: Create the introduction as subtitles.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-subed-intro-subtitles</span> ()
  <span class="org-doc">"Create the introduction as subtitles."</span>
  (<span class="org-keyword">interactive</span>)
  (subed-auto-insert)
  (<span class="org-keyword">let</span> ((emacsconf-publishing-phase <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">conference</span>))
    (mapc
     (<span class="org-keyword">lambda</span> (sub) (apply <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">subed-append-subtitle</span> nil (cdr sub)))
     (seq-map-indexed
      (<span class="org-keyword">lambda</span> (talk i)
        (list
         nil
         (* i 5000)
         (1- (* i 5000))
         (format <span class="org-string">"#+OUTPUT: %s.webm\n[[file:%s]]\n%s"</span>
                 (plist-get talk <span class="org-builtin">:slug</span>)
                 (expand-file-name
                  (concat (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">".svg.png"</span>)
                  (expand-file-name <span class="org-string">"in-between"</span> emacsconf-stream-asset-dir))
                 (emacsconf-pad-expand-intro talk))))
      (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))))
</pre></div></details>
<p></p>
</div>
</div>
<div id="outline-container-org62207a6" class="outline-2">
<h3 id="org62207a6">Links</h3>
<div class="outline-text-2" id="text-org62207a6">
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/">Using subed-record in Emacs to edit audio and clean up oopses</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="http://emacsconf.org/2023/talks">EmacsConf - 2023 - Talks</a></li>
<li><a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-stream.el">emacsconf-stream.el</a></li>
</ul>

<p>
Yay Emacs!
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/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%2Femacsconf-backstage-making-lots-of-intro-videos-with-subed-record%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>
	</channel>
</rss>