<?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 - python</title>
	<atom:link href="https://sachachua.com/blog/category/python/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/python" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/python/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Fri, 08 Nov 2024 01:48:10 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>Updating Planet Venus so that planet.emacslife.com can handle mix-blend-mode in my SVGs</title>
		<link>https://sachachua.com/blog/2024/01/updating-planet-venus-so-that-planet-emacslife-com-can-handle-mix-blend-mode-in-my-svgs/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 26 Jan 2024 14:30:12 GMT</pubDate>
    <category>geek</category>
<category>python</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/updating-planet-venus-so-that-planet-emacslife-com-can-handle-mix-blend-mode-in-my-svgs/</guid>
		<description><![CDATA[<p>
I wanted to easily turn segments from my Yay Emacs
livestreams or recorded narration into
closed-caption audio and dynamic highlighting of
my SVG sketches and text transcripts. That way,
people could easily jump around to sections
they're interested in.
</p>

<p>
Not everyone has Javascript turned on, so I wanted
to start with something that made sense even in
RSS feeds like the one on <a href="https://planet.emacslife.com">Planet Emacslife</a> (which
strips out <code>&lt;style&gt;</code> and <code>&lt;script&gt;</code>) and was
progressively enhanced with captions and
highlighting if you saw it on my site.
</p>


<figure id="org2be8a9c">
<img src="https://sachachua.com/blog/2024/01/updating-planet-venus-so-that-planet-emacslife-com-can-handle-mix-blend-mode-in-my-svgs/2024-01-25_08-37-11.png" alt="2024-01-25_08-37-11.png">

<figcaption><span class="figure-number">Figure 1: </span>My SVGs were broken: black fill, no mix-blend-mode</figcaption>
</figure>

<p>
The blog aggregator I'm using, <a href="https://github.com/rubys/venus">Planet Venus</a>,
hasn't been updated in 14 years. It even uses
Python 2. I considered switching to a different
aggregator, so I started checking out different
community planets. Most of the other planets
listed in this <a href="https://news.ycombinator.com/item?id=4929490">HN thread about aggregators</a> looked
like they were using the same Planet Venus
aggregator, although these were some planets that
used something else:
</p>
<ul class="org-ul">
<li><a href="https://github.com/hrw/very-simple-planet-aggregator">https://github.com/hrw/very-simple-planet-aggregator</a></li>
<li><a href="https://github.com/openSUSE/planet-o-o">https://github.com/openSUSE/planet-o-o</a></li>
<li><a href="https://planet.lisp.org/about.html">https://planet.lisp.org/about.html</a></li>
</ul>

<p>
I decided I'd stick with Planet Venus for now,
since I could probably figure out how to get the
attributes sorted out.
</p>

<p>
I found <code>planet/vendor/feedparser.py</code> by digging
around. Adding <code>mix-blend-mode</code> to the list of
attributes there was not enough to get it working.
I started exploring <a href="https://realpython.com/emacs-the-best-python-editor/">pdb</a> for interactive Python
debugging inside Emacs, although I think <a href="https://www.reddit.com/r/emacs/comments/k5dsar/emacs_ide_for_python_setting_up_the_debugger_with/">dap</a> is an
option too. I wrote a short bit of code to test things out:
</p>


<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> sys,os
sys.path.insert(1, os.path.join(os.path.dirname(__file__), <span class="org-string">'planet/vendor'</span>))
<span class="org-keyword">from</span> feedparser <span class="org-keyword">import</span> _sanitizeHTML
<span class="org-keyword">assert</span> <span class="org-string">'strong'</span> <span class="org-keyword">in</span> _sanitizeHTML(<span class="org-string">'&lt;strong&gt;Hello&lt;/strong&gt;'</span>, <span class="org-string">'utf-8'</span>, <span class="org-string">'text/html'</span>)
<span class="org-keyword">assert</span> <span class="org-string">'mix-blend-mode'</span> <span class="org-keyword">in</span> _sanitizeHTML(<span class="org-string">'&lt;svg&gt;&lt;path style="fill: red;mix-blend-mode:darken"&gt;&lt;/path&gt;&lt;/svg&gt;'</span>, <span class="org-string">'utf-8'</span>, <span class="org-string">'text/html'</span>)
</pre>
</div>


<p>
It was pretty easy to use pdb to start stepping
through and into functions, although I didn't dig
into it deeply because I figured it out another
way. While looking through the pull requests for
the Venus repository, I came across this
<a href="https://github.com/rubys/venus/pull/19">pull
request to add data- attributes</a> which was
helpful because it pointed me to
<code>planet/vendor/html5lib/sanitizer.py</code>. Once I
added <code>mix-blend-mode</code> to that one, things worked.
<a href="https://github.com/sachac/venus/tree/sachac-additional-attribs">Here's my Github branch</a>.
</p>


<figure id="org627461a">
<img src="https://sachachua.com/blog/2024/01/updating-planet-venus-so-that-planet-emacslife-com-can-handle-mix-blend-mode-in-my-svgs/2024-01-25_08-51-33.png" alt="2024-01-25_08-51-33.png">

<figcaption><span class="figure-number">Figure 2: </span>planet.emacslife.com now lets me use mix-blend-mode</figcaption>
</figure>

<p>
On a somewhat related note, I also had to <a href="https://sachachua.com/blog/2024/01/patching-elfeed-and-shr-to-handle-svg-images-with-viewbox-attributes/">patching
shr to handle SVG images with viewBox attributes</a>.
I guess SVGs aren't that common yet, but I'm
looking forward to playing around with them more,
so I might as well make things better (at least
when it comes to things I can actually tweak).
<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode#browser_compatibility">mix-blend-mode on SVG elements</a> says it's not
supported in Safari or a bunch of mobile browsers,
but it seems to be working on my phone, so maybe
that's cool now. Using mix-blend-mode means I
don't have to do something complicated when it
comes to animating highlights while still keeping
text visible, and improving SVG support is the
right thing to do. Onward!
</p>
<div><a href="https://sachachua.com/blog/2024/01/updating-planet-venus-so-that-planet-emacslife-com-can-handle-mix-blend-mode-in-my-svgs/index.org">View org source for this post</a></div>]]></description>
		</item><item>
		<title>Summarizing #EmacsConf's growth over 5 years by year, and making an animated GIF</title>
		<link>https://sachachua.com/blog/2023/10/summarizing-emacsconf-s-growth-over-5-years-by-year-and-making-an-animated-gif/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 06 Oct 2023 21:59:36 GMT</pubDate>
    <category>emacs</category>
<category>emacsconf</category>
<category>python</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/10/summarizing-emacsconf-s-growth-over-5-years-by-year-and-making-an-animated-gif/</guid>
		<description><![CDATA[<p>
Of course, after I <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/">charted EmacsConf's growth in terms of number of
submissions and minutes</a>, I realized I also wanted to just sum
everything up by year. So here it is:
</p>

<div class="org-src-container">
<pre class="src src-python" id="org0956942"><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> df.drop(<span class="org-string">'Weeks to CFP'</span>, axis<span class="org-operator">=</span>1).groupby([<span class="org-string">'Year'</span>]).<span class="org-builtin">sum</span>()
<span class="org-variable-name">fig</span>, <span class="org-variable-name">ax</span> <span class="org-operator">=</span> plt.subplots(nrows<span class="org-operator">=</span>1, ncols<span class="org-operator">=</span>2, figsize<span class="org-operator">=</span>(12,6))
<span class="org-variable-name">fig1</span> <span class="org-operator">=</span> df[<span class="org-string">'Count'</span>].plot(kind<span class="org-operator">=</span><span class="org-string">"bar"</span>, ax<span class="org-operator">=</span>ax[0], title<span class="org-operator">=</span><span class="org-string">'Number of submissions'</span>)
<span class="org-variable-name">fig2</span> <span class="org-operator">=</span> df[<span class="org-string">'Minutes'</span>].plot(kind<span class="org-operator">=</span><span class="org-string">"bar"</span>, ax<span class="org-operator">=</span>ax[1], title<span class="org-operator">=</span><span class="org-string">'Number of minutes'</span>)
fig.get_figure().savefig(<span class="org-string">'emacsconf-by-year.png'</span>)
<span class="org-keyword">return</span> df
</pre>
</div>

<div class="datatable" id="orgc3eb9f6">
<table>


<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">Count</th>
<th scope="col" class="org-right">Minutes</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">2019</td>
<td class="org-right">28</td>
<td class="org-right">429</td>
</tr>

<tr>
<td class="org-right">2020</td>
<td class="org-right">35</td>
<td class="org-right">699</td>
</tr>

<tr>
<td class="org-right">2021</td>
<td class="org-right">44</td>
<td class="org-right">578</td>
</tr>

<tr>
<td class="org-right">2022</td>
<td class="org-right">29</td>
<td class="org-right">512</td>
</tr>

<tr>
<td class="org-right">2023</td>
<td class="org-right">39</td>
<td class="org-right">730</td>
</tr>
</tbody>
</table>

</div>


<figure id="orgf0345ea">
<img src="https://sachachua.com/blog/2023/10/summarizing-emacsconf-s-growth-over-5-years-by-year-and-making-an-animated-gif/emacsconf-by-year.png" alt="emacsconf-by-year.png">

</figure>

<p>
I also wanted to make an animated GIF so that the cumulative graphs
could be a little easier to understand.
</p>

<div class="org-src-container">
<pre class="src src-python" id="orgda430d3"><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-keyword">import</span> imageio <span class="org-keyword">as</span> io
<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">fig</span>, <span class="org-variable-name">ax</span> <span class="org-operator">=</span> plt.subplots(nrows<span class="org-operator">=</span>1, ncols<span class="org-operator">=</span>2, figsize<span class="org-operator">=</span>(12,6))
<span class="org-variable-name">count</span> <span class="org-operator">=</span> pd.pivot_table(df, columns<span class="org-operator">=</span>[<span class="org-string">'Year'</span>], index<span class="org-operator">=</span>[<span class="org-string">'Weeks to CFP'</span>], values<span class="org-operator">=</span><span class="org-string">'Count'</span>, aggfunc<span class="org-operator">=</span><span class="org-string">'sum'</span>, fill_value<span class="org-operator">=</span>0).iloc[::<span class="org-operator">-</span>1].sort_index(ascending<span class="org-operator">=</span><span class="org-constant">True</span>).cumsum()
<span class="org-variable-name">minutes</span> <span class="org-operator">=</span> pd.pivot_table(df, columns<span class="org-operator">=</span>[<span class="org-string">'Year'</span>], index<span class="org-operator">=</span>[<span class="org-string">'Weeks to CFP'</span>], values<span class="org-operator">=</span><span class="org-string">'Minutes'</span>, aggfunc<span class="org-operator">=</span><span class="org-string">'sum'</span>, fill_value<span class="org-operator">=</span>0).iloc[::<span class="org-operator">-</span>1].sort_index(ascending<span class="org-operator">=</span><span class="org-constant">True</span>).cumsum()
ax[0].set_ylim([0, count.<span class="org-builtin">max</span>().<span class="org-builtin">max</span>()])
ax[1].set_ylim([0, minutes.<span class="org-builtin">max</span>().<span class="org-builtin">max</span>()])
<span class="org-keyword">with</span> io.get_writer(<span class="org-string">'emacsconf-combined.gif'</span>, mode<span class="org-operator">=</span><span class="org-string">'I'</span>, duration<span class="org-operator">=</span>[500, 500, 500, 500, 1000], loop<span class="org-operator">=</span>0) <span class="org-keyword">as</span> writer:
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> year <span class="org-keyword">in</span> <span class="org-builtin">range</span>(2019, 2024):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   count[year].plot(ax<span class="org-operator">=</span>ax[0], title<span class="org-operator">=</span><span class="org-string">'Cumulative submissions'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   minutes[year].plot(ax<span class="org-operator">=</span>ax[1], title<span class="org-operator">=</span><span class="org-string">'Cumulative minutes'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   ax[0].legend(loc<span class="org-operator">=</span><span class="org-string">'upper left'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   ax[1].legend(loc<span class="org-operator">=</span><span class="org-string">'upper left'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> axis <span class="org-keyword">in</span> ax:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> line <span class="org-keyword">in</span> axis.get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> line <span class="org-keyword">in</span> axis.legend().get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)        
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">filename</span> <span class="org-operator">=</span> f<span class="org-string">'emacsconf-combined-$</span>{year}<span class="org-string">.png'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   fig.get_figure().savefig(filename)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">image</span> <span class="org-operator">=</span> io.v3.imread(filename)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   writer.append_data(image)
</pre>
</div>


<figure id="orge21e4ae">
<img src="https://sachachua.com/blog/2023/10/summarizing-emacsconf-s-growth-over-5-years-by-year-and-making-an-animated-gif/emacsconf-combined.gif" alt="emacsconf-combined.gif">

<figcaption><span class="figure-number">Figure 1: </span>Animated GIF showing the cumulative total submissions and minutes</figcaption>
</figure>

<p>
I am not quite sure what kind of story this data tells (aside from the
fact that there sure are a lot of great talks), but it was fun to
learn how to make more kinds of graphs and animate them too. Could be
useful someday. =)
</p>
]]></description>
		</item><item>
		<title>#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</title>
		<link>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/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 06 Oct 2023 21:00:34 GMT</pubDate>
    <category>emacsconf</category>
<category>emacs</category>
<category>org</category>
<category>python</category>
		<guid isPermaLink="false">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/</guid>
		<description><![CDATA[<p>
Having helped organize EmacsConf for a number of years now, I know
that I usually panic about whether we have submissions partway through
the call for participation. This causes us to extend the CFP deadline
and ask people to ask more people to submit things, and then we end up
with a wonderful deluge of talks that I then have to somehow squeeze
into a reasonable-looking schedule.
</p>

<p>
This year, I managed to not panic <i>and</i> I also resisted the urge to extend the
CFP deadline, trusting that there will actually be tons of cool stuff.
It helped that my <a href="https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/">schedule SVG code</a> let me visualize what the
conference could feel like with the submissions so far, so we started
with a reasonably nice one-track conference and built up from there.
It also helped that I'd gone back to the submissions for 2022 and
plotted them by the number of weeks before the CFP deadline, and I
knew that there'd be a big spike from all those people whose Org
<code>DEADLINE:</code> properties would nudge them into finalizing their
proposals.
</p>

<p>
Out of curiosity, I wanted to see how the stats for this year compared
with previous years. I wrote a small function to collect the data that I wanted to summarize:
</p>

<p>
</p><details><summary>emacsconf-count-submissions-by-week: Count submissions in INFO by distance to CFP-DEADLINE.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-count-submissions-by-week</span> (<span class="org-type">&amp;optional</span> info cfp-deadline)
  <span class="org-doc">"Count submissions in INFO by distance to CFP-DEADLINE."</span>
  (<span class="org-keyword">setq</span> cfp-deadline (<span class="org-keyword">or</span> cfp-deadline emacsconf-cfp-deadline))
  (<span class="org-keyword">setq</span> info (<span class="org-keyword">or</span> info (emacsconf-get-talk-info)))
  (cons <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"Weeks to CFP end date"</span> <span class="org-string">"Count"</span> <span class="org-string">"Hours"</span>)
        (mapcar (<span class="org-keyword">lambda</span> (entry)
                  (list (car entry)
                        (length (cdr entry))
                        (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">cdr</span> (cdr entry)))))
                (seq-group-by
                 <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">car</span>
                 (sort
                  (seq-keep
                   (<span class="org-keyword">lambda</span> (o)
                     (<span class="org-keyword">and</span> (emacsconf-publish-talk-p o)
                          (plist-get o <span class="org-builtin">:date-submitted</span>)
                          (cons (floor (/ (days-between (plist-get o <span class="org-builtin">:date-submitted</span>) cfp-deadline)
                                          7.0))
                                (string-to-number
                                 (<span class="org-keyword">or</span> (plist-get o <span class="org-builtin">:video-duration</span>)
                                     (plist-get o <span class="org-builtin">:time</span>)
                                     <span class="org-string">"0"</span>)))))
                   info)
                  (<span class="org-keyword">lambda</span> (a b) (&lt; (car a) (car b))))))))
</pre></div></details>
<p></p>

<p>
and then I ran it against the different files for each year, filling
in the previous years' data as needed. The resulting table is pretty
long, so I've put that in a collapsible section.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp" id="orgcfb56da">(<span class="org-keyword">let</span> ((years <span class="org-highlight-quoted-quote">`</span>((2023 <span class="org-string">"~/proj/emacsconf/2023/private/conf.org"</span> <span class="org-string">"2023-09-15"</span>)
               (2022 <span class="org-string">"~/proj/emacsconf/2022/private/conf.org"</span> <span class="org-string">"2022-09-18"</span>)
               (2021 <span class="org-string">"~/proj/emacsconf/2021/private/conf.org"</span> <span class="org-string">"2021-09-30"</span>)
               (2020 <span class="org-string">"~/proj/emacsconf/wiki/2020/submissions.org"</span> <span class="org-string">"2020-09-30"</span>)
               (2019 <span class="org-string">"~/proj/emacsconf/2019/private/conf.org"</span> <span class="org-string">"2019-08-31"</span>))))
  (append
   <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"Weeks to CFP"</span> <span class="org-string">"Year"</span> <span class="org-string">"Count"</span> <span class="org-string">"Minutes"</span>))
   (seq-mapcat
    (<span class="org-keyword">lambda</span> (year-info)
      (<span class="org-keyword">let</span> ((emacsconf-org-file (elt year-info 1))
            (emacsconf-cfp-deadline (elt year-info 2))
            (year (car year-info)))
        (mapcar (<span class="org-keyword">lambda</span> (o) (list (car o) year (cadr o) (elt o 2)))
                (cdr (emacsconf-count-submissions-by-week (emacsconf-get-talk-info) emacsconf-cfp-deadline)))))
    years)))
</pre>
</div>

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


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

<col class="org-right">

<col class="org-right">

<col class="org-right">
</colgroup>
<tbody>
<tr>
<td class="org-right">Weeks to CFP</td>
<td class="org-right">Year</td>
<td class="org-right">Count</td>
<td class="org-right">Minutes</td>
</tr>

<tr>
<td class="org-right">-12</td>
<td class="org-right">2023</td>
<td class="org-right">4</td>
<td class="org-right">70</td>
</tr>

<tr>
<td class="org-right">-9</td>
<td class="org-right">2023</td>
<td class="org-right">2</td>
<td class="org-right">30</td>
</tr>

<tr>
<td class="org-right">-7</td>
<td class="org-right">2023</td>
<td class="org-right">2</td>
<td class="org-right">30</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2023</td>
<td class="org-right">2</td>
<td class="org-right">30</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">2023</td>
<td class="org-right">2</td>
<td class="org-right">60</td>
</tr>

<tr>
<td class="org-right">-3</td>
<td class="org-right">2023</td>
<td class="org-right">3</td>
<td class="org-right">40</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">2023</td>
<td class="org-right">5</td>
<td class="org-right">130</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">2023</td>
<td class="org-right">10</td>
<td class="org-right">180</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">2023</td>
<td class="org-right">8</td>
<td class="org-right">140</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">2023</td>
<td class="org-right">1</td>
<td class="org-right">20</td>
</tr>

<tr>
<td class="org-right">-8</td>
<td class="org-right">2022</td>
<td class="org-right">2</td>
<td class="org-right">25</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2022</td>
<td class="org-right">2</td>
<td class="org-right">31</td>
</tr>

<tr>
<td class="org-right">-3</td>
<td class="org-right">2022</td>
<td class="org-right">2</td>
<td class="org-right">31</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">2022</td>
<td class="org-right">2</td>
<td class="org-right">17</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">2022</td>
<td class="org-right">8</td>
<td class="org-right">191</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">2022</td>
<td class="org-right">8</td>
<td class="org-right">110</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">2022</td>
<td class="org-right">5</td>
<td class="org-right">107</td>
</tr>

<tr>
<td class="org-right">-8</td>
<td class="org-right">2021</td>
<td class="org-right">4</td>
<td class="org-right">50</td>
</tr>

<tr>
<td class="org-right">-7</td>
<td class="org-right">2021</td>
<td class="org-right">2</td>
<td class="org-right">17</td>
</tr>

<tr>
<td class="org-right">-6</td>
<td class="org-right">2021</td>
<td class="org-right">1</td>
<td class="org-right">7</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2021</td>
<td class="org-right">2</td>
<td class="org-right">22</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">2021</td>
<td class="org-right">2</td>
<td class="org-right">19</td>
</tr>

<tr>
<td class="org-right">-3</td>
<td class="org-right">2021</td>
<td class="org-right">5</td>
<td class="org-right">73</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">2021</td>
<td class="org-right">1</td>
<td class="org-right">10</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">2021</td>
<td class="org-right">12</td>
<td class="org-right">163</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">2021</td>
<td class="org-right">13</td>
<td class="org-right">197</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">2021</td>
<td class="org-right">1</td>
<td class="org-right">10</td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-right">2021</td>
<td class="org-right">1</td>
<td class="org-right">10</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2020</td>
<td class="org-right">1</td>
<td class="org-right">10</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">2020</td>
<td class="org-right">1</td>
<td class="org-right">15</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">2020</td>
<td class="org-right">1</td>
<td class="org-right">30</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">2020</td>
<td class="org-right">4</td>
<td class="org-right">68</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">2020</td>
<td class="org-right">21</td>
<td class="org-right">424</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">2020</td>
<td class="org-right">7</td>
<td class="org-right">152</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2019</td>
<td class="org-right">2</td>
<td class="org-right">45</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">2019</td>
<td class="org-right">1</td>
<td class="org-right">21</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">2019</td>
<td class="org-right">6</td>
<td class="org-right">126</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">2019</td>
<td class="org-right">9</td>
<td class="org-right">82</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">2019</td>
<td class="org-right">9</td>
<td class="org-right">148</td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-right">2019</td>
<td class="org-right">1</td>
<td class="org-right">7</td>
</tr>
</tbody>
</table>


</details>

<p>
Some talks were proposed off-list and are not captured here, and
cancelled or withdrawn talks weren't included either. The times for
previous years use the actual video time, and the times for this year
use proposed times.
</p>

<p>
Off the top of my head, I didn't know of an easy way to make a pivot
table or cross-tab using just Org Mode or Emacs Lisp. I tried using
<a href="https://www.gnu.org/software/datamash/manual/html_node/Crosstab.html">datamash</a>, but I was having a hard time getting my output just the way
I wanted it. Fortunately, it was super-easy to get my data from an Org
table into Python so I could use <a href="https://pandas.pydata.org/docs/reference/api/pandas.pivot_table.html">pandas.pivot_table</a>. Because I had
used <code>#+NAME: submissions-by-week</code> to label the table, I could use
<code>:var data=submissions-by-week</code> to refer to the data in my Python
program. Then I could summarize them by week.
</p>

<p>
Here's the number of submissions by the number of weeks to the
original CFP deadline, so we can see people generally like to target
the CFP date.
</p>

<div class="org-src-container">
<pre class="src src-python" id="org840fd87"><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">'Year'</span>], index<span class="org-operator">=</span>[<span class="org-string">'Weeks to CFP'</span>], values<span class="org-operator">=</span><span class="org-string">'Count'</span>, aggfunc<span class="org-operator">=</span><span class="org-string">'sum'</span>, fill_value<span class="org-operator">=</span>0).iloc[::<span class="org-operator">-</span>1].sort_index(ascending<span class="org-operator">=</span><span class="org-constant">True</span>)
<span class="org-variable-name">fig</span>, <span class="org-variable-name">ax</span> <span class="org-operator">=</span> plt.subplots()
<span class="org-variable-name">figure</span> <span class="org-operator">=</span> df.plot(title<span class="org-operator">=</span><span class="org-string">'Number of submissions by number of weeks to the CFP end date'</span>, ax<span class="org-operator">=</span>ax)
<span class="org-keyword">for</span> line <span class="org-keyword">in</span> ax.get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)
<span class="org-keyword">for</span> line <span class="org-keyword">in</span> plt.legend().get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)        
figure.get_figure().savefig(<span class="org-string">'number-of-submissions.png'</span>)
<span class="org-keyword">return</span> df
</pre>
</div>

<div class="row">
<div class="columns small-12 medium-6 large-6">
<div class="datatable" id="orgd80b013">
<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">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Weeks to CFP</th>
<th scope="col" class="org-right">2019</th>
<th scope="col" class="org-right">2020</th>
<th scope="col" class="org-right">2021</th>
<th scope="col" class="org-right">2022</th>
<th scope="col" class="org-right">2023</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">-12</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">4</td>
</tr>

<tr>
<td class="org-right">-9</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-right">-8</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">4</td>
<td class="org-right">2</td>
<td class="org-right">0</td>
</tr>

<tr>
<td class="org-right">-7</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">2</td>
<td class="org-right">0</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-right">-6</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">1</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2</td>
<td class="org-right">1</td>
<td class="org-right">2</td>
<td class="org-right">2</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">1</td>
<td class="org-right">1</td>
<td class="org-right">2</td>
<td class="org-right">0</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-right">-3</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">5</td>
<td class="org-right">2</td>
<td class="org-right">3</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">6</td>
<td class="org-right">1</td>
<td class="org-right">1</td>
<td class="org-right">2</td>
<td class="org-right">5</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">9</td>
<td class="org-right">4</td>
<td class="org-right">12</td>
<td class="org-right">8</td>
<td class="org-right">10</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">9</td>
<td class="org-right">21</td>
<td class="org-right">13</td>
<td class="org-right">8</td>
<td class="org-right">8</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">0</td>
<td class="org-right">7</td>
<td class="org-right">1</td>
<td class="org-right">5</td>
<td class="org-right">1</td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-right">1</td>
<td class="org-right">0</td>
<td class="org-right">1</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
</tr>
</tbody>
</table>

</div>

</div>
<div class="columns small-12 medium-6 large-6">

<figure id="orgd2eddd8">
<img src="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/number-of-submissions.png" alt="number-of-submissions.png">

</figure>

</div>

</div>

<p>
Calculating the cumulative number of submissions might be more useful.
Here, each row shows the number received so far.
</p>

<div class="org-src-container">
<pre class="src src-python" id="orgfd7e017"><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">'Year'</span>], index<span class="org-operator">=</span>[<span class="org-string">'Weeks to CFP'</span>], values<span class="org-operator">=</span><span class="org-string">'Count'</span>, aggfunc<span class="org-operator">=</span><span class="org-string">'sum'</span>, fill_value<span class="org-operator">=</span>0).iloc[::<span class="org-operator">-</span>1].sort_index(ascending<span class="org-operator">=</span><span class="org-constant">True</span>).cumsum()
<span class="org-variable-name">fig</span>, <span class="org-variable-name">ax</span> <span class="org-operator">=</span> plt.subplots()
<span class="org-variable-name">figure</span> <span class="org-operator">=</span> df.plot(title<span class="org-operator">=</span><span class="org-string">'Cumulative submissions by number of weeks to the CFP end date'</span>, ax<span class="org-operator">=</span>ax)
<span class="org-keyword">for</span> line <span class="org-keyword">in</span> ax.get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)
<span class="org-keyword">for</span> line <span class="org-keyword">in</span> plt.legend().get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)        
figure.get_figure().savefig(<span class="org-string">'cumulative-submissions.png'</span>)
<span class="org-keyword">return</span> df
</pre>
</div>

<div class="row">
<div class="columns small-12 medium-6 large-6">
<div class="datatable" id="org99360ee">
<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">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Weeks to CFP</th>
<th scope="col" class="org-right">2019</th>
<th scope="col" class="org-right">2020</th>
<th scope="col" class="org-right">2021</th>
<th scope="col" class="org-right">2022</th>
<th scope="col" class="org-right">2023</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">-12</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">4</td>
</tr>

<tr>
<td class="org-right">-9</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">6</td>
</tr>

<tr>
<td class="org-right">-8</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">4</td>
<td class="org-right">2</td>
<td class="org-right">6</td>
</tr>

<tr>
<td class="org-right">-7</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">6</td>
<td class="org-right">2</td>
<td class="org-right">8</td>
</tr>

<tr>
<td class="org-right">-6</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">7</td>
<td class="org-right">2</td>
<td class="org-right">8</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">2</td>
<td class="org-right">1</td>
<td class="org-right">9</td>
<td class="org-right">4</td>
<td class="org-right">10</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">3</td>
<td class="org-right">2</td>
<td class="org-right">11</td>
<td class="org-right">4</td>
<td class="org-right">12</td>
</tr>

<tr>
<td class="org-right">-3</td>
<td class="org-right">3</td>
<td class="org-right">2</td>
<td class="org-right">16</td>
<td class="org-right">6</td>
<td class="org-right">15</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">9</td>
<td class="org-right">3</td>
<td class="org-right">17</td>
<td class="org-right">8</td>
<td class="org-right">20</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">18</td>
<td class="org-right">7</td>
<td class="org-right">29</td>
<td class="org-right">16</td>
<td class="org-right">30</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">27</td>
<td class="org-right">28</td>
<td class="org-right">42</td>
<td class="org-right">24</td>
<td class="org-right">38</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">27</td>
<td class="org-right">35</td>
<td class="org-right">43</td>
<td class="org-right">29</td>
<td class="org-right">39</td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-right">28</td>
<td class="org-right">35</td>
<td class="org-right">44</td>
<td class="org-right">29</td>
<td class="org-right">39</td>
</tr>
</tbody>
</table>

</div>

</div>
<div class="columns small-12 medium-6 large-6">

<figure id="orge02d1cd">
<img src="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/cumulative-submissions.png" alt="cumulative-submissions.png">

<figcaption><span class="figure-number">Figure 1: </span>Cumulative submissions by number of weeks to CFP end date</figcaption>
</figure>

</div>

</div>

<p>
And here's the cumulative number of minutes based on the proposals.
</p>

<div class="org-src-container">
<pre class="src src-python" id="orgd55abea"><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">'Year'</span>], index<span class="org-operator">=</span>[<span class="org-string">'Weeks to CFP'</span>], values<span class="org-operator">=</span><span class="org-string">'Minutes'</span>, aggfunc<span class="org-operator">=</span><span class="org-string">'sum'</span>, fill_value<span class="org-operator">=</span>0).iloc[::<span class="org-operator">-</span>1].sort_index(ascending<span class="org-operator">=</span><span class="org-constant">True</span>).cumsum()
<span class="org-variable-name">fig</span>, <span class="org-variable-name">ax</span> <span class="org-operator">=</span> plt.subplots()
<span class="org-variable-name">figure</span> <span class="org-operator">=</span> df.plot(title<span class="org-operator">=</span><span class="org-string">'Cumulative minutes by number of weeks to the CFP end date'</span>, ax<span class="org-operator">=</span>ax)
<span class="org-keyword">for</span> line <span class="org-keyword">in</span> ax.get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)
<span class="org-keyword">for</span> line <span class="org-keyword">in</span> plt.legend().get_lines():
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> line.get_label() <span class="org-operator">==</span> <span class="org-string">'2023'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   line.set_linewidth(5)        
figure.get_figure().savefig(<span class="org-string">'cumulative-minutes.png'</span>)
<span class="org-keyword">return</span> df
</pre>
</div>

<div class="row">
<div class="columns small-12 medium-6 large-6">
<div class="datatable" id="orgd677ee2">
<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">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Weeks to CFP</th>
<th scope="col" class="org-right">2019</th>
<th scope="col" class="org-right">2020</th>
<th scope="col" class="org-right">2021</th>
<th scope="col" class="org-right">2022</th>
<th scope="col" class="org-right">2023</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">-12</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">70</td>
</tr>

<tr>
<td class="org-right">-9</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">100</td>
</tr>

<tr>
<td class="org-right">-8</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">50</td>
<td class="org-right">25</td>
<td class="org-right">100</td>
</tr>

<tr>
<td class="org-right">-7</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">67</td>
<td class="org-right">25</td>
<td class="org-right">130</td>
</tr>

<tr>
<td class="org-right">-6</td>
<td class="org-right">0</td>
<td class="org-right">0</td>
<td class="org-right">74</td>
<td class="org-right">25</td>
<td class="org-right">130</td>
</tr>

<tr>
<td class="org-right">-5</td>
<td class="org-right">45</td>
<td class="org-right">10</td>
<td class="org-right">96</td>
<td class="org-right">56</td>
<td class="org-right">160</td>
</tr>

<tr>
<td class="org-right">-4</td>
<td class="org-right">66</td>
<td class="org-right">25</td>
<td class="org-right">115</td>
<td class="org-right">56</td>
<td class="org-right">220</td>
</tr>

<tr>
<td class="org-right">-3</td>
<td class="org-right">66</td>
<td class="org-right">25</td>
<td class="org-right">188</td>
<td class="org-right">87</td>
<td class="org-right">260</td>
</tr>

<tr>
<td class="org-right">-2</td>
<td class="org-right">192</td>
<td class="org-right">55</td>
<td class="org-right">198</td>
<td class="org-right">104</td>
<td class="org-right">390</td>
</tr>

<tr>
<td class="org-right">-1</td>
<td class="org-right">274</td>
<td class="org-right">123</td>
<td class="org-right">361</td>
<td class="org-right">295</td>
<td class="org-right">570</td>
</tr>

<tr>
<td class="org-right">0</td>
<td class="org-right">422</td>
<td class="org-right">547</td>
<td class="org-right">558</td>
<td class="org-right">405</td>
<td class="org-right">710</td>
</tr>

<tr>
<td class="org-right">1</td>
<td class="org-right">422</td>
<td class="org-right">699</td>
<td class="org-right">568</td>
<td class="org-right">512</td>
<td class="org-right">730</td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-right">429</td>
<td class="org-right">699</td>
<td class="org-right">578</td>
<td class="org-right">512</td>
<td class="org-right">730</td>
</tr>
</tbody>
</table>

</div>

</div>
<div class="columns small-12 medium-6 large-6">

<figure id="org8cc8c9b">
<img src="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/cumulative-minutes.png" alt="cumulative-minutes.png">

<figcaption><span class="figure-number">Figure 2: </span>Cumulative minutes by number of weeks to the CFP end date</figcaption>
</figure>

</div>

</div>

<p>
So&#x2026; yeah&#x2026; 730 minutes of talks for this year&#x2026; I might've gotten
a little carried away. But I like all the talks! And I want them to be
captured in videos and maybe even transcribed by people who will take
the time to change misrecognized words like Emax into Emacs! And I
want people to be able to connect with other people who are interested
in the sorts of stuff they're doing! So we're going to make it happen.
The <a href="https://emacsconf.org/2023/organizers-notebook/#draft-schedule">draft schedule</a>'s looking pretty full, but I think it'll work out,
especially if the speakers send in their videos on time. Let's see how
it all works out!
</p>

<style>
.datatable td, .datatable th { border: 1px solid #ccc }
</style>

<p>
(&#x2026;and look, I even got to learn how to do pivot tables and graphs with
Python!)
</p>
]]></description>
		</item><item>
		<title>Resetting the Python logger level</title>
		<link>https://sachachua.com/blog/2023/03/resetting-the-python-logger-level/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 23 Mar 2023 21:46:16 GMT</pubDate>
    <category>python</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/03/resetting-the-python-logger-level/</guid>
		<description><![CDATA[<p>
I wanted to get extra debugging output from a Python script that I was
running, but one of my imports seemed to mess things up and the logger
level was 20 (info) instead of debug.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> logging
<span class="org-keyword">from</span> llama_index <span class="org-keyword">import</span> GPTSimpleVectorIndex, MockLLMPredictor, MockEmbedding, QuestionAnswerPrompt
logging.basicConfig(level<span class="org-operator">=</span>logging.DEBUG)
logging.debug(<span class="org-string">'This is a test.'</span>)
</pre>
</div>

<p>
As it turns out, there was already <a href="https://github.com/jerryjliu/llama_index/issues/720">a call to basicConfig in
<code>github_repository_reader.py</code> which <code>llama_index</code> loaded at some
point</a>, and the <a href="https://docs.python.org/3/howto/logging.html">Python documentation</a> says: "As it’s intended as a
one-off simple configuration facility, only the first call will
actually do anything: subsequent calls are effectively no-ops."
</p>

<p>
So this is what I needed to do instead:
</p>

<div class="org-src-container">
<pre class="src src-python">logging.getLogger().setLevel(logging.DEBUG)
</pre>
</div>
]]></description>
		</item><item>
		<title>Rename, recolor, and file my sketches automatically</title>
		<link>https://sachachua.com/blog/2023/01/recolor-rename-and-file-my-sketches-automatically/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Wed, 04 Jan 2023 17:16:46 GMT</pubDate>
    <category>geek</category>
<category>supernote</category>
<category>python</category>
<category>drawing</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/01/recolor-rename-and-file-my-sketches-automatically/</guid>
		<description><![CDATA[<p>
I want to make it easier to process the sketchnotes I make on my
Supernote. I write IDs of the form <code>yyyy-mm-dd-nn</code> to identify my
sketches. To avoid duplicates, I get these IDs from the web-based
journaling system I wrote. I've started putting the titles and tags
into those journal entries as well so that I can reuse them in
scripts. When I export a sketch to PNG and synchronize it, the file
appears in my <code>~/Dropbox/Supernote/EXPORT</code> directory on my laptop.
Then it goes through this process:
</p>

<ul class="org-ul">
<li>I use <a href="https://cloud.google.com/vision/docs/handwriting">Google Cloud Vision</a> to detect handwriting so that I can find the ID.
<ul class="org-ul">
<li>I retrieve the matching entry from my journal
system and rename the file based on the title and tags.</li>
<li>If there's no matching entry, I rename the file based on the ID.</li>
</ul></li>
<li>If there are other tags or references in the sketch, I add those to the filename as well.</li>
<li>I recolor it based on the tags, so parenting-related posts are a little purple, tech/Emacs-related posts are blue, and things are generally highlighted in yellow otherwise.</li>
<li>I move it to a directory based on the tags.
<ul class="org-ul">
<li>If it's a private sketch, I move it to the directory for my private sketches.</li>
<li>If it's a public sketch, I move it to the directory that will eventually get synchronized to <a href="https://sketches.sachachua.com">sketches.sachachua.com</a>, and I reload the list of sketches after some delay.</li>
</ul></li>
</ul>

<p>
The following code does that processing.
</p>

<p>
<a href="https://sachachua.com/blog/2023/01/recolor-rename-and-file-my-sketches-automatically/supernote-daemon">Download supernote-daemon</a>
</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>supernote-daemon source code</strong></summary>
<div class="org-src-container">
<pre class="src src-python"><span class="org-comment-delimiter">#</span><span class="org-comment">!/usr/bin/python3</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">-*- mode: python -*-</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">(c) 2022-2023 Sacha Chua (sacha@sachachua.com) - MIT License</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Permission is hereby granted, free of charge, to any person</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">obtaining a copy of this software and associated documentation files</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">(the "Software"), to deal in the Software without restriction,</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">including without limitation the rights to use, copy, modify, merge,</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">publish, distribute, sublicense, and/or sell copies of the Software,</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">and to permit persons to whom the Software is furnished to do so,</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">subject to the following conditions:</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">The above copyright notice and this permission notice shall be</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">included in all copies or substantial portions of the Software.</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">SOFTWARE.</span>


<span class="org-keyword">import</span> os
<span class="org-keyword">import</span> json
<span class="org-keyword">import</span> re
<span class="org-keyword">import</span> requests
<span class="org-keyword">import</span> time
<span class="org-keyword">from</span> dotenv <span class="org-keyword">import</span> load_dotenv
<span class="org-comment-delimiter"># </span><span class="org-comment">Import the Google Cloud client libraries</span>
<span class="org-keyword">from</span> google.cloud <span class="org-keyword">import</span> vision
<span class="org-keyword">from</span> google.cloud.vision_v1 <span class="org-keyword">import</span> AnnotateImageResponse
<span class="org-keyword">import</span> sys
sys.path.append(<span class="org-string">"/home/sacha/proj/supernote/"</span>)
<span class="org-keyword">import</span> recolor   <span class="org-comment-delimiter"># </span><span class="org-comment">noqa: E402  # muffles flake8 error about import</span>
load_dotenv()


<span class="org-comment-delimiter"># </span><span class="org-comment">Set the folder path where the png files are located</span>
<span class="org-variable-name">folder_path</span> = <span class="org-string">'/home/sacha/Dropbox/Supernote/EXPORT/'</span>
<span class="org-variable-name">public_sketch_dir</span> = <span class="org-string">'/home/sacha/sync/sketches/'</span>
<span class="org-variable-name">private_sketch_dir</span> = <span class="org-string">'/home/sacha/sync/private-sketches/'</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Initialize the Google Cloud Vision client</span>
<span class="org-variable-name">client</span> = vision.ImageAnnotatorClient()
<span class="org-variable-name">refresh_counter</span> = 0

<span class="org-keyword">def</span> <span class="org-function-name">extract_text</span>(client, <span class="org-builtin">file</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">json_file</span> = <span class="org-builtin">file</span>[:-3] + <span class="org-string">'json'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">TODO Preprocess to keep only black text</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">with</span> <span class="org-builtin">open</span>(<span class="org-builtin">file</span>, <span class="org-string">'rb'</span>) <span class="org-keyword">as</span> image_file:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">content</span> = image_file.read()
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Convert the png file to a Google Cloud Vision image object</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">image</span> = vision.Image(content=content)

<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Extract handwriting from the image using the Google Cloud Vision API</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">response</span> = client.document_text_detection(image=image)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">response_json</span> = AnnotateImageResponse.to_json(response)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">json_response</span> = json.loads(response_json)
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Save the response to a json file with the same name as the png file</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">with</span> <span class="org-builtin">open</span>(json_file, <span class="org-string">"w"</span>) <span class="org-keyword">as</span> f:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   json.dump(json_response, f)


<span class="org-keyword">def</span> <span class="org-function-name">maybe_rename</span>(<span class="org-builtin">file</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">TODO Match on ID</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">json_file</span> = <span class="org-builtin">file</span>[:-3] + <span class="org-string">'json'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">with</span> <span class="org-builtin">open</span>(json_file, <span class="org-string">'r'</span>) <span class="org-keyword">as</span> f:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">data</span> = json.load(f)

<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Extract the text from the json file</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">text</span> = data[<span class="org-string">'fullTextAnnotation'</span>][<span class="org-string">'text'</span>]

<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Check if the text contains a string matching the regex pattern</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">pattern</span> = r<span class="org-string">'(?&lt;!ref:)[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">match</span> = re.search(pattern, text)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-keyword">match</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Get the matched string</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">matched_string</span> = <span class="org-keyword">match</span>.group(0)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_name</span> = matched_string
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">from_zid</span> = get_journal_entry(matched_string).strip()
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> from_zid:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_name</span> = matched_string + <span class="org-string">' '</span> + from_zid
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">tags</span> = get_tags(new_name, text)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> tags:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_name</span> = new_name + <span class="org-string">' '</span> + tags
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">ref</span> = get_references(text)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> ref:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_name</span> = new_name + <span class="org-string">' '</span> + ref
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">'Renaming '</span> + <span class="org-builtin">file</span> + <span class="org-string">' to '</span> + new_name)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Rename the png and json files to the matched string</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_filename</span> = os.path.join(os.path.dirname(<span class="org-builtin">file</span>), new_name + <span class="org-string">'.png'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   rename_set(<span class="org-builtin">file</span>, new_filename)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> new_filename


<span class="org-keyword">def</span> <span class="org-function-name">get_tags</span>(filename, text):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">tags</span> = re.findall(r<span class="org-string">'(^|\W)#[ \n\t]+'</span>, text)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> <span class="org-string">' '</span>.join(<span class="org-builtin">filter</span>(<span class="org-keyword">lambda</span> x: x <span class="org-keyword">not</span> <span class="org-keyword">in</span> filename, tags))


<span class="org-keyword">def</span> <span class="org-function-name">get_references</span>(text):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">refs</span> = re.findall(r<span class="org-string">'!ref:[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}'</span>, text)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> <span class="org-string">' '</span>.join(refs)


<span class="org-keyword">def</span> <span class="org-function-name">get_journal_entry</span>(zid):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">resp</span> = requests.get(<span class="org-string">'https://'</span> + os.environ[<span class="org-string">'JOURNAL_USER'</span>]
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   + <span class="org-string">':'</span> + os.environ[<span class="org-string">'JOURNAL_PASS'</span>]
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   + <span class="org-string">'@journal.sachachua.com/api/entries/'</span> + zid)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">j</span> = resp.json()
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> j <span class="org-keyword">and</span> <span class="org-keyword">not</span> re.search(<span class="org-string">'^I thought about'</span>, j[<span class="org-string">'Note'</span>]):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> j[<span class="org-string">'Note'</span>]


<span class="org-keyword">def</span> <span class="org-function-name">get_color_map</span>(filename, text=<span class="org-constant">None</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> text:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">together</span> = filename + <span class="org-string">' '</span> + text
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">together</span> = filename
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> re.search(<span class="org-string">'r#(parenting|purple|life)'</span>, together):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> {<span class="org-string">'9d9d9d'</span>: <span class="org-string">'8754a1'</span>, <span class="org-string">'c9c9c9'</span>: <span class="org-string">'e4c1d9'</span>}  <span class="org-comment-delimiter"># </span><span class="org-comment">parenting is purplish</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">elif</span> re.search(r<span class="org-string">'#(emacs|geek|tech|blue)'</span>, together):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> {<span class="org-string">'9d9d9d'</span>: <span class="org-string">'2b64a9'</span>, <span class="org-string">'c9c9c9'</span>: <span class="org-string">'b3e3f1'</span>}  <span class="org-comment-delimiter"># </span><span class="org-comment">geeky stuff in light/dark blue</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> {<span class="org-string">'9d9d9d'</span>: <span class="org-string">'884636'</span>, <span class="org-string">'c9c9c9'</span>: <span class="org-string">'f6f396'</span>}  <span class="org-comment-delimiter"># </span><span class="org-comment">yellow highlighter, dark brown</span>


<span class="org-keyword">def</span> <span class="org-function-name">rename_set</span>(old_name, new_name):
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> old_name != new_name:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">old_json</span> = old_name[:-3] + <span class="org-string">'json'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_json</span> = new_name[:-3] + <span class="org-string">'json'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   os.rename(old_name, new_name)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   os.rename(old_json, new_json)


<span class="org-keyword">def</span> <span class="org-function-name">recolor_based_on_filename</span>(filename):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">color_map</span> = get_color_map(filename)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">recolored</span> = recolor.map_colors(filename, color_map)
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">possibly rename based on the filename</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_filename</span> = re.sub(<span class="org-string">' #(purple|blue)'</span>, <span class="org-string">''</span>, filename)
<span class="org-highlight-indentation"> </span>   rename_set(filename, new_filename)
<span class="org-highlight-indentation"> </span>   recolored.save(new_filename)


<span class="org-keyword">def</span> <span class="org-function-name">move_processed_sketch</span>(<span class="org-builtin">file</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">global</span> refresh_counter
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-string">'#private'</span> <span class="org-keyword">in</span> <span class="org-builtin">file</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">output_dir</span> = private_sketch_dir
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">elif</span> <span class="org-string">'#'</span> <span class="org-keyword">in</span> <span class="org-builtin">file</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">output_dir</span> = public_sketch_dir
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">refresh_counter</span> = 3
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> <span class="org-builtin">file</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_filename</span> = os.path.join(output_dir, os.path.basename(<span class="org-builtin">file</span>))
<span class="org-highlight-indentation"> </span>   rename_set(<span class="org-builtin">file</span>, new_filename)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> new_filename


<span class="org-keyword">def</span> <span class="org-function-name">process_file</span>(<span class="org-builtin">file</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">json_file</span> = <span class="org-builtin">file</span>[:-3] + <span class="org-string">'json'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Check if a corresponding json file already exists</span>
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-keyword">not</span> os.path.exists(json_file):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   extract_text(client, <span class="org-builtin">file</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-keyword">not</span> re.search(<span class="org-string">'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2} '</span>, <span class="org-builtin">file</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">file</span> = maybe_rename(<span class="org-builtin">file</span>)
<span class="org-highlight-indentation"> </span>   recolor_based_on_filename(<span class="org-builtin">file</span>)
<span class="org-highlight-indentation"> </span>   move_processed_sketch(<span class="org-builtin">file</span>)


<span class="org-keyword">def</span> <span class="org-function-name">process_dir</span>(folder_path):
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">global</span> processed_files
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Iterate through all png files in the specified folder</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">files</span> = <span class="org-builtin">sorted</span>(os.listdir(folder_path))
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> <span class="org-builtin">file</span> <span class="org-keyword">in</span> files:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-builtin">file</span>.endswith(<span class="org-string">'.png'</span>) <span class="org-keyword">and</span> <span class="org-string">'_'</span> <span class="org-keyword">in</span> <span class="org-builtin">file</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"Processing "</span>, <span class="org-builtin">file</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   process_file(os.path.join(folder_path, <span class="org-builtin">file</span>))


<span class="org-keyword">def</span> <span class="org-function-name">daemon</span>(folder_path, wait):
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">global</span> refresh_counter
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">while</span> <span class="org-constant">True</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   process_dir(folder_path)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   time.sleep(wait)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> refresh_counter &gt; 0:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">refresh_counter</span> = refresh_counter - 1
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> refresh_counter == 0:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"Reloading sketches"</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   requests.get(<span class="org-string">'https://'</span> + os.environ[<span class="org-string">'JOURNAL_USER'</span>] + <span class="org-string">':'</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>+ os.environ[<span class="org-string">'JOURNAL_PASS'</span>]
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>+ <span class="org-string">'@sketches.sachachua.com/reload?python=1'</span>)


<span class="org-keyword">if</span> <span class="org-builtin">__name__</span> == <span class="org-string">'__main__'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Create a set to store the names of processed files</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">processed_files</span> = <span class="org-builtin">set</span>()
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-builtin">len</span>(sys.argv) &gt; 1:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> os.path.isdir(sys.argv[1]):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">folder_path</span> = sys.argv[1]
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   daemon(folder_path, 300)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> f <span class="org-keyword">in</span> sys.argv[1:]:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   process_file(f)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   daemon(folder_path, 300)
</pre>
</div>


</details>

<p>
It uses this script I wrote to <a href="https://sachachua.com/blog/2022/08/recoloring-my-sketches-with-python/">recolor my sketches with Python</a>.
</p>

<p>
I'm contemplating writing some annotation tools to make it easier to
turn the detected text into useful text for searching or writing about
because the sketches throw off the recognition (misrecognized text,
low confidence) and the columns mess up the line wrapping. Low priority, though.
</p>

<p>
My handwriting (at least for numbers) is probably simple enough that I
might be able to train Tesseract OCR to process that someday. And who
knows, maybe some organization will release a pre-trained model for
offline handwriting recognition that'll be as useful as OpenAI Whisper
is for audio files. That would be neat!
</p>
]]></description>
		</item><item>
		<title>Using Emacs and Python to record an animation and synchronize it with audio</title>
		<link>https://sachachua.com/blog/2022/12/using-emacs-and-python-to-record-an-animation-and-synchronize-it-with-audio/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 24 Dec 2022 02:20:36 GMT</pubDate>
    <category>emacs</category>
<category>emacsconf</category>
<category>python</category>
<category>subed</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2022/12/using-emacs-and-python-to-record-an-animation-and-synchronize-it-with-audio/</guid>
		<description><![CDATA[<div class="update" id="org736ffe8">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2023-01-14 Sat]</span></span>: Removed my fork since upstream now has the :eval function.
</p>

</div>

<p>
The Q&amp;A session for <a href="https://emacsconf.org/2022/rms">Things I'd like to see in Emacs</a> (Richard Stallman) from EmacsConf 2022 was done over Mumble. Amin pasted the questions into the Mumble chat buffer and I copied them into a larger buffer as the speaker answered them, but I didn't do it consistently. I figured it might be worth making another video with easier-to-read visuals. At first, I thought about using LaTeX to create Beamer slides with the question text, which I could then turn into a video using ffmpeg. Then I decided to figure out how to animate the text in Emacs, because why not? I figured a straightforward typing animation would probably be less distracting than <code>animate-string</code>, and <a href="https://github.com/bard/emacs-director">emacs-director</a> seems to handle that nicely. I <a href="https://github.com/sachac/emacs-director">forked</a> it to add a few things I wanted, like variables to make the typing speed slower (so that it could more reliably type things on my old laptop, since sometimes the timers seemed to have hiccups) <del>and an <code>:eval</code> step for running things without needing to log them</del>. (2023-01-14: Upstream has the :eval feature now.)
</p>

<p>
To make it easy to synchronize the resulting animation with the chapter markers I derived from the transcript of the audio file, I decided to beep between scenes. First step: make a beep file.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -f lavfi -i <span class="org-string">'sine=frequency=1000:duration=0.1'</span> beep.wav
</pre>
</div>

<p>
Next, I animated the text, with a beep between scenes. I used
<code>subed-parse-file</code> to read the question text directly from <a href="https://emacsconf.org/2022/captions/emacsconf-2022-rms&#45;&#45;what-id-like-to-see-in-emacs&#45;&#45;answers&#45;&#45;chapters.vtt">the chapter
markers</a>, and I used <a href="https://www.maartenbaert.be/simplescreenrecorder/">simplescreenrecorder</a> to set up the recording
settings (including audio).
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-beep</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">save-window-excursion</span>
    (shell-command <span class="org-string">"aplay ~/recordings/beep.wav &amp;"</span> nil nil)))

(<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">director</span>)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-recording-process</span> nil)
(shell-command <span class="org-string">"xdotool getwindowfocus windowsize 1282 720"</span>)
(<span class="org-keyword">progn</span>
  (switch-to-buffer (get-buffer-create <span class="org-string">"*Questions*"</span>))
  (erase-buffer)
  (org-mode)
  (face-remap-add-relative <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> <span class="org-builtin">:height</span> 300)
  (<span class="org-keyword">setq-local</span> mode-line-format <span class="org-string">"   Q&amp;A for EmacsConf 2022: What I'd like to see in Emacs (Richard M. Stallman) - emacsconf.org/2022/talks/rms"</span>)
  (sit-for 3)
  (delete-other-windows)
  (hl-line-mode -1)
  (<span class="org-keyword">when</span> (process-live-p emacsconf-recording-process) (kill-process emacsconf-recording-process))
  (<span class="org-keyword">setq</span> emacsconf-recording-process (start-process <span class="org-string">"ssr"</span> (get-buffer-create <span class="org-string">"*ssr*"</span>)
                                                   <span class="org-string">"simplescreenrecorder"</span>
                                                   <span class="org-string">"&#45;&#45;start-recording"</span>
                                                   <span class="org-string">"&#45;&#45;start-hidden"</span>))
  (sit-for 3)
  (director-run
   <span class="org-builtin">:version</span> 1
   <span class="org-builtin">:log-target</span> <span class="org-highlight-quoted-quote">'</span>(file . <span class="org-string">"/tmp/director.log"</span>)
   <span class="org-builtin">:before-start</span>
   (<span class="org-keyword">lambda</span> ()
     (switch-to-buffer (get-buffer-create <span class="org-string">"*Questions*"</span>))
     (delete-other-windows))
   <span class="org-builtin">:steps</span>
   (<span class="org-keyword">let</span> ((subtitles (subed-parse-file <span class="org-string">"~/proj/emacsconf/rms/emacsconf-2022-rms&#45;&#45;what-id-like-to-see-in-emacs&#45;&#45;answers&#45;&#45;chapters.vtt"</span>)))
     (apply <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">append</span>
            (list
             (list <span class="org-builtin">:eval</span> <span class="org-highlight-quoted-quote">'</span>(my-beep))
             (list <span class="org-builtin">:type</span> <span class="org-string">"* Q&amp;A for Richard Stallman's EmacsConf 2022 talk: What I'd like to see in Emacs\nhttps://emacsconf.org/2022/talks/rms\n\n"</span>))
            (mapcar
             (<span class="org-keyword">lambda</span> (sub)
               (list
                (list <span class="org-builtin">:log</span> (elt sub 3))
                (list <span class="org-builtin">:eval</span> <span class="org-highlight-quoted-quote">'</span>(progn (org-end-of-subtree)
                                    (<span class="org-keyword">unless</span> (bolp) (insert <span class="org-string">"\n"</span>))))
                (list <span class="org-builtin">:type</span> (concat <span class="org-string">"** "</span> (elt sub 3) <span class="org-string">"\n\n"</span>))
                (list <span class="org-builtin">:eval</span> <span class="org-highlight-quoted-quote">'</span>(org-back-to-heading))
                (list <span class="org-builtin">:wait</span> 5)
                (list <span class="org-builtin">:eval</span> <span class="org-highlight-quoted-quote">'</span>(my-beep))))
             subtitles)))
   <span class="org-builtin">:typing-style</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">human</span>
   <span class="org-builtin">:delay-between-steps</span> 0
   <span class="org-builtin">:after-end</span> (<span class="org-keyword">lambda</span> ()
                (process-send-string emacsconf-recording-process <span class="org-string">"record-save\nwindow-show\nquit\n"</span>))
   <span class="org-builtin">:on-failure</span> (<span class="org-keyword">lambda</span> ()
                 (process-send-string emacsconf-recording-process <span class="org-string">"record-save\nwindow-show\nquit\n"</span>))
   <span class="org-builtin">:on-error</span> (<span class="org-keyword">lambda</span> ()
               (process-send-string emacsconf-recording-process <span class="org-string">"record-save\nwindow-show\nquit\n"</span>))))
</pre>
</div>

<p>
I used the following code to copy the latest recording to <code>animation.webm</code> and extract the audio to <code>animation.wav</code>. <code>my-latest-file</code> and <code>my-recordings-dir</code> are in <a href="https://sachachua.com/dotemacs">my Emacs config</a>.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((name <span class="org-string">"animation.webm"</span>))
  (copy-file (my-latest-file my-recordings-dir) name t)
  (shell-command
   (format <span class="org-string">"ffmpeg -y -i %s -ar 8000 -ac 1 %s.wav"</span>
           (shell-quote-argument name)
           (shell-quote-argument (file-name-sans-extension name)))))
</pre>
</div>

<p>
Then I needed to get the timestamps of the beeps in the recording. I subtracted a little bit (<code>0.82</code> seconds) based on comparing the waveform with the results.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-variable-name">filename</span> <span class="org-operator">=</span> <span class="org-string">"animation.wav"</span>
<span class="org-keyword">from</span> scipy.io <span class="org-keyword">import</span> wavfile
<span class="org-keyword">from</span> scipy <span class="org-keyword">import</span> signal
<span class="org-keyword">import</span> numpy <span class="org-keyword">as</span> np
<span class="org-keyword">import</span> re
<span class="org-variable-name">rate</span>, <span class="org-variable-name">source</span> <span class="org-operator">=</span> wavfile.read(filename)
<span class="org-variable-name">peaks</span> <span class="org-operator">=</span> signal.find_peaks(source, height<span class="org-operator">=</span>1000, distance<span class="org-operator">=</span>1000)
<span class="org-variable-name">base_times</span> <span class="org-operator">=</span> (peaks[0] <span class="org-operator">/</span> rate) <span class="org-operator">-</span> 0.82
<span class="org-builtin">print</span>(base_times)
</pre>
</div>

<p>
I noticed that the first question didn't seem to get beeped properly, so I tweaked the times. Then I wrote some code to generate a very long ffmpeg command that used trim and tpad to select the segments and extend them to the right durations. There was some drift when I did it without the audio track, but the timestamps seemed to work right when I included the Q&amp;A audio track as well.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> webvtt
<span class="org-keyword">import</span> subprocess
<span class="org-variable-name">chapters_filename</span> <span class="org-operator">=</span>  <span class="org-string">"emacsconf-2022-rms&#45;&#45;what-id-like-to-see-in-emacs&#45;&#45;answers&#45;&#45;chapters.vtt"</span>
<span class="org-variable-name">answers_filename</span> <span class="org-operator">=</span> <span class="org-string">"answers.wav"</span>
<span class="org-variable-name">animation_filename</span> <span class="org-operator">=</span> <span class="org-string">"animation.webm"</span>
<span class="org-keyword">def</span> <span class="org-function-name">get_length</span>(filename):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">result</span> <span class="org-operator">=</span> subprocess.run([<span class="org-string">"ffprobe"</span>, <span class="org-string">"-v"</span>, <span class="org-string">"error"</span>, <span class="org-string">"-show_entries"</span>,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span><span class="org-string">"format=duration"</span>, <span class="org-string">"-of"</span>,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span><span class="org-string">"default=noprint_wrappers=1:nokey=1"</span>, filename],
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   stdout<span class="org-operator">=</span>subprocess.PIPE,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   stderr<span class="org-operator">=</span>subprocess.STDOUT)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> <span class="org-builtin">float</span>(result.stdout)

<span class="org-keyword">def</span> <span class="org-function-name">get_frames</span>(filename):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">result</span> <span class="org-operator">=</span> subprocess.run([<span class="org-string">"ffprobe"</span>, <span class="org-string">"-v"</span>, <span class="org-string">"error"</span>, <span class="org-string">"-select_streams"</span>, <span class="org-string">"v:0"</span>, <span class="org-string">"-count_packets"</span>,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span><span class="org-string">"-show_entries"</span>, <span class="org-string">"stream=nb_read_packets"</span>, <span class="org-string">"-of"</span>,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span><span class="org-string">"csv=p=0"</span>, filename],
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   stdout<span class="org-operator">=</span>subprocess.PIPE,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   stderr<span class="org-operator">=</span>subprocess.STDOUT)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> <span class="org-builtin">float</span>(result.stdout)

<span class="org-variable-name">answers_length</span> <span class="org-operator">=</span> get_length(answers_filename)
<span class="org-comment-delimiter"># </span><span class="org-comment">override base_times</span>
<span class="org-variable-name">times</span> <span class="org-operator">=</span> np.asarray([  1.515875,  13.50, 52.32125 ,  81.368625, 116.66625 , 146.023125,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  161.904875, 182.820875, 209.92125 , 226.51525 , 247.93875 ,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  260.971   , 270.87375 , 278.23325 , 303.166875, 327.44925 ,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  351.616375, 372.39525 , 394.246625, 409.36325 , 420.527875,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  431.854   , 440.608625, 473.86825 , 488.539   , 518.751875,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  544.1515  , 555.006   , 576.89225 , 598.157375, 627.795125,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  647.187125, 661.10875 , 695.87175 , 709.750125, 717.359875])
<span class="org-variable-name">fps</span> <span class="org-operator">=</span> 30.0
<span class="org-variable-name">times</span> <span class="org-operator">=</span> np.append(times, get_length(animation_filename))
<span class="org-variable-name">anim_spans</span> <span class="org-operator">=</span> <span class="org-builtin">list</span>(<span class="org-builtin">zip</span>(times[:<span class="org-operator">-</span>1], times[1:]))
<span class="org-variable-name">chapters</span> <span class="org-operator">=</span> webvtt.read(chapters_filename)
<span class="org-keyword">if</span> chapters[0].start_in_seconds <span class="org-operator">==</span> 0:
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">vtt_times</span> <span class="org-operator">=</span> [[c.start_in_seconds, c.text] <span class="org-keyword">for</span> c <span class="org-keyword">in</span> chapters]
<span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">vtt_times</span> <span class="org-operator">=</span> [[0, <span class="org-string">"Introduction"</span>]] <span class="org-operator">+</span> [[c.start_in_seconds, c.text] <span class="org-keyword">for</span> c <span class="org-keyword">in</span> chapters] 
<span class="org-variable-name">vtt_times</span> <span class="org-operator">=</span> vtt_times <span class="org-operator">+</span> [[answers_length, <span class="org-string">"End"</span>]]
<span class="org-comment-delimiter"># </span><span class="org-comment">Add ending timestamps</span>
<span class="org-variable-name">vtt_times</span> <span class="org-operator">=</span> [[x[0][0], x[1][0], x[0][1]] <span class="org-keyword">for</span> x <span class="org-keyword">in</span> <span class="org-builtin">zip</span>(vtt_times[:<span class="org-operator">-</span>1], vtt_times[1:])]
<span class="org-variable-name">test_rate</span> <span class="org-operator">=</span> 1.0

<span class="org-variable-name">i</span> <span class="org-operator">=</span> 0
<span class="org-variable-name">concat_list</span> <span class="org-operator">=</span> <span class="org-string">""</span>
<span class="org-variable-name">groups</span> <span class="org-operator">=</span> <span class="org-builtin">list</span>(<span class="org-builtin">zip</span>(anim_spans, vtt_times))
<span class="org-keyword">import</span> ffmpeg
<span class="org-variable-name">animation</span> <span class="org-operator">=</span> ffmpeg.<span class="org-builtin">input</span>(<span class="org-string">'animation.webm'</span>).video
<span class="org-variable-name">audio</span> <span class="org-operator">=</span> ffmpeg.<span class="org-builtin">input</span>(<span class="org-string">'rms.opus'</span>)

<span class="org-variable-name">for_overlay</span> <span class="org-operator">=</span> ffmpeg.<span class="org-builtin">input</span>(<span class="org-string">'color=color=black:size=1280x720:d=%f'</span> <span class="org-operator">%</span> answers_length, f<span class="org-operator">=</span><span class="org-string">'lavfi'</span>)
<span class="org-variable-name">params</span> <span class="org-operator">=</span> {<span class="org-string">"b:v"</span>: <span class="org-string">"1k"</span>, <span class="org-string">"vcodec"</span>: <span class="org-string">"libvpx"</span>, <span class="org-string">"r"</span>: <span class="org-string">"30"</span>, <span class="org-string">"crf"</span>: <span class="org-string">"63"</span>}
<span class="org-variable-name">test_limit</span> <span class="org-operator">=</span> 1
<span class="org-variable-name">params</span> <span class="org-operator">=</span> {<span class="org-string">"vcodec"</span>: <span class="org-string">"libvpx"</span>, <span class="org-string">"r"</span>: <span class="org-string">"30"</span>, <span class="org-string">"copyts"</span>: <span class="org-constant">None</span>, <span class="org-string">"b:v"</span>: <span class="org-string">"1M"</span>, <span class="org-string">"crf"</span>: 24}
<span class="org-variable-name">test_limit</span> <span class="org-operator">=</span> 0
<span class="org-variable-name">anim_rate</span> <span class="org-operator">=</span> 1
<span class="org-keyword">import</span> math
<span class="org-variable-name">cursor</span> <span class="org-operator">=</span> 0
<span class="org-keyword">if</span> test_limit <span class="org-operator">&gt;</span> 0:
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">groups</span> <span class="org-operator">=</span> groups[0:test_limit]
<span class="org-variable-name">clips</span> <span class="org-operator">=</span> []

<span class="org-comment-delimiter"># </span><span class="org-comment">cursor is the current time</span>
<span class="org-keyword">for</span> anim, vtt <span class="org-keyword">in</span> groups:
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">padding</span> <span class="org-operator">=</span> vtt[1] <span class="org-operator">-</span> cursor <span class="org-operator">-</span> (anim[1] <span class="org-operator">-</span> anim[0]) <span class="org-operator">/</span> anim_rate
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> (padding <span class="org-operator">&lt;</span> 0):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"Squeezing"</span>, math.floor((anim[1] <span class="org-operator">-</span> anim[0]) <span class="org-operator">/</span> (anim_rate <span class="org-operator">*</span> 1.0)), <span class="org-string">'into'</span>, vtt[1] <span class="org-operator">-</span> cursor, padding)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   clips.append(animation.trim(start<span class="org-operator">=</span>anim[0], end<span class="org-operator">=</span>anim[1]).setpts(<span class="org-string">'PTS-STARTPTS'</span>)) 
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">elif</span> padding <span class="org-operator">==</span> 0:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   clips.append(animation.trim(start<span class="org-operator">=</span>anim[0], end<span class="org-operator">=</span>anim[1]).setpts(<span class="org-string">'PTS-STARTPTS'</span>))
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"%f to %f: Padding %f into %f - pad: %f"</span> <span class="org-operator">%</span> (cursor, vtt[1], (anim[1] <span class="org-operator">-</span> anim[0]) <span class="org-operator">/</span> (anim_rate <span class="org-operator">*</span> 1.0), vtt[1] <span class="org-operator">-</span> cursor, padding))
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">cursor</span> <span class="org-operator">=</span> cursor <span class="org-operator">+</span> padding <span class="org-operator">+</span> (anim[1] <span class="org-operator">-</span> anim[0]) <span class="org-operator">/</span> anim_rate
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   clips.append(animation.trim(start<span class="org-operator">=</span>anim[0], end<span class="org-operator">=</span>anim[1]).setpts(<span class="org-string">'PTS-STARTPTS'</span>).<span class="org-builtin">filter</span>(<span class="org-string">'tpad'</span>, stop_mode<span class="org-operator">=</span><span class="org-string">"clone"</span>, stop_duration<span class="org-operator">=</span>padding))
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">for_overlay</span> <span class="org-operator">=</span> for_overlay.overlay(animation.trim(start<span class="org-operator">=</span>anim[0], end<span class="org-operator">=</span>anim[1]).setpts(<span class="org-string">'PTS-STARTPTS+%f'</span> <span class="org-operator">%</span> vtt[0]))
<span class="org-highlight-indentation"> </span>   clips.append(audio.<span class="org-builtin">filter</span>(<span class="org-string">'atrim'</span>, start<span class="org-operator">=</span>vtt[0], end<span class="org-operator">=</span>vtt[1]).<span class="org-builtin">filter</span>(<span class="org-string">'asetpts'</span>, <span class="org-string">'PTS-STARTPTS'</span>))
<span class="org-variable-name">args</span> <span class="org-operator">=</span> ffmpeg.concat(<span class="org-operator">*</span>clips, v<span class="org-operator">=</span>1, a<span class="org-operator">=</span>1).output(<span class="org-string">'output.webm'</span>, <span class="org-operator">**</span>params).overwrite_output().<span class="org-builtin">compile</span>()
<span class="org-builtin">print</span>(<span class="org-string">' '</span>.join(f<span class="org-string">'"</span>{item}<span class="org-string">"'</span> <span class="org-keyword">for</span> item <span class="org-keyword">in</span> args))
</pre>
</div>

<p>
Anyway, it's here for future reference. =)
</p>
<div><a href="https://sachachua.com/blog/2022/12/using-emacs-and-python-to-record-an-animation-and-synchronize-it-with-audio/index.org">View org source for this post</a></div>]]></description>
		</item><item>
		<title>Avoiding automatic data type conversion in Microsoft Excel and Pandas</title>
		<link>https://sachachua.com/blog/2021/12/avoiding-automatic-data-type-conversion-in-microsoft-excel-and-pandas/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 24 Dec 2021 23:53:07 GMT</pubDate>
    <category>coding</category>
<category>python</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2021/12/avoiding-automatic-data-type-conversion-in-microsoft-excel-and-pandas/</guid>
		<description><![CDATA[<p>
Automatic conversion of data types is often handy, but sometimes it
can mess things up. For example, when you import a CSV into Microsoft
Excel, it will helpfully convert and display dates/times in your
preferred format&#x2013;and it will use your configured format when
exporting back to CSV, which is not cool when your original file had
<code>YYYY-MM-DD HH:MM:SS</code> and someone's computer decided to turn it into
<code>MM/DD/YY HH:MM</code>. To avoid this conversion and import the columns as
strings, you can change the file extension to <code>.txt</code> instead of <code>.csv</code>
and then change each column type that you care about, which can be a
lot of clicking. I had to change things back with a regular expression
along the lines of:
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> re
<span class="org-variable-name">s</span> = <span class="org-string">"12/9/21 11:23"</span>
<span class="org-variable-name">match</span> = re.match(<span class="org-string">'([0-9]+)/([0-9]+)/([0-9]+)( [0-9]+:[0-9]+)'</span>, s)
<span class="org-variable-name">date</span> = <span class="org-string">'20%s-%s-%s%s:00'</span> % (match.group(3).zfill(2), match.group(1).zfill(2), match.group(2).zfill(2), match.group(4))
<span class="org-keyword">print</span>(date)
</pre>
</div>

<p>
The <code>pandas</code> library for Python also likes to do this kind of data
type conversion for data types and for NaN values. In this particular
situation, I wanted it to leave columns alone and leave the <code>nan</code>
string in my input alone. Otherwise, <code>to_csv</code> would replace <code>nan</code> with
the blank string, which could mess up a different script that used
this data as input. This is the code to do it:
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> pandas <span class="org-keyword">as</span> pd
<span class="org-variable-name">df</span> = pd.read_csv(<span class="org-string">'filename.csv'</span>, encoding=<span class="org-string">'utf-8'</span>, dtype=<span class="org-builtin">str</span>, na_filter=<span class="org-constant">False</span>)
</pre>
</div>

<p>
I'm probably going to run into this again sometime, so I wanted to
make sure I put my notes somewhere I can find them later.
</p>
]]></description>
		</item>
	</channel>
</rss>