<?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 - subed</title>
	<atom:link href="https://sachachua.com/blog/category/subed/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/subed" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/subed/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>A git post-commit hook for tagging my subed.el release version</title>
		<link>https://sachachua.com/blog/2024/10/a-git-post-commit-hook-for-tagging-my-subed-el-release-version/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Wed, 23 Oct 2024 12:53:44 GMT</pubDate>
    <category>git</category>
<category>emacs</category>
<category>subed</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/10/a-git-post-commit-hook-for-tagging-my-subed-el-release-version/</guid>
		<description><![CDATA[<p>
Debian uses <a href="https://github.com/sachac/subed/issues/76">Git repository tags</a> to notice when to update packages. <a href="https://github.com/sachac/subed/issues/77">I kept forgetting to tag subed's versions</a>, so now I made a git post-commit hook which I think will do the trick.
I based it on <a href="https://gist.github.com/ajmirsky/1245103">https://gist.github.com/ajmirsky/1245103</a>, just updated for Python 3 and tweaked to work with how I do versions in subed.el. I've also added it to my <a href="https://github.com/sachac/subed/blob/main/subed/README.org">README.org</a>.
</p>


<div class="org-src-container">
<pre class="src src-python"><span class="org-comment-delimiter">#</span><span class="org-comment">!/usr/bin/python</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">place in .git/hooks/post-commit</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">Based on https://gist.github.com/ajmirsky/1245103</span>

<span class="org-keyword">import</span> subprocess
<span class="org-keyword">import</span> re

<span class="org-builtin">print</span>(<span class="org-string">"checking for version change..."</span>,)

<span class="org-variable-name">output</span> <span class="org-operator">=</span> subprocess.check_output([<span class="org-string">'git'</span>, <span class="org-string">'diff'</span>, <span class="org-string">'HEAD^'</span>, <span class="org-string">'HEAD'</span>, <span class="org-string">'-U0'</span>]).decode(<span class="org-string">"utf-8"</span>)

<span class="org-variable-name">version_info</span> <span class="org-operator">=</span> <span class="org-constant">None</span>
<span class="org-keyword">for</span> d <span class="org-keyword">in</span> output.split(<span class="org-string">"</span><span class="org-constant">\n</span><span class="org-string">"</span>):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">rg</span> <span class="org-operator">=</span> re.<span class="org-builtin">compile</span>(r<span class="org-string">'\+(?:;;\s+)?Version:\s+(?P&lt;major&gt;[0-9]+)\.(?P&lt;minor&gt;[0-9]+)\.(?P&lt;rev&gt;[0-9]+)'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">m</span> <span class="org-operator">=</span> rg.search(d)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> m:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">version_info</span> <span class="org-operator">=</span> m.groupdict()
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">break</span>

<span class="org-keyword">if</span> version_info:
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">tag</span> <span class="org-operator">=</span> <span class="org-string">"v%s.%s.%s"</span> <span class="org-operator">%</span> (version_info[<span class="org-string">'major'</span>], version_info[<span class="org-string">'minor'</span>], version_info[<span class="org-string">'rev'</span>])
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">existing</span> <span class="org-operator">=</span> subprocess.check_output([<span class="org-string">'git'</span>, <span class="org-string">'tag'</span>]).decode(<span class="org-string">"utf-8"</span>).split(<span class="org-string">"</span><span class="org-constant">\n</span><span class="org-string">"</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> tag <span class="org-keyword">in</span> existing:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"%s is already tagged, not updating"</span> <span class="org-operator">%</span> tag)
<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">result</span> <span class="org-operator">=</span> subprocess.run([<span class="org-string">'git'</span>, <span class="org-string">'tag'</span>, <span class="org-string">'-f'</span>, tag])
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> result.returncode:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">raise</span> <span class="org-type">Exception</span>(<span class="org-string">'tagging not successful: %s %s'</span> <span class="org-operator">%</span> (result.stdout, result.returncode))
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"tagged revision: %s"</span> <span class="org-operator">%</span> tag)
<span class="org-keyword">else</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"none found."</span>)
</pre>
</div>

<div><a href="https://sachachua.com/blog/2024/10/a-git-post-commit-hook-for-tagging-my-subed-el-release-version/index.org">View org source for this post</a></div>]]></description>
		</item><item>
		<title>Yay Emacs 5: Tweaking my video workflow with WhisperX and subed-record</title>
		<link>https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Mon, 07 Oct 2024 18:16:08 GMT</pubDate>
    <category>emacs</category>
<category>subed</category>
<category>yay-emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/</guid>
		<description><![CDATA[<div class="row"><div class="columns"><div style="width: 400px"><video controls="1" src="https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/video-workflow.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/video-workflow.webm">Download the video</a></video></div></div><div class="columns">
<p>
I'm tweaking my video workflow. I use Orgzly Revived on my Android phone to write the text, and I use Easy Voice Recorder to record it. Syncthing automatically copies both to my laptop. I use WhisperX to transcribe my recording, and I use a little bit of Emacs Lisp to figure out timestamps for each word. I edit this to fix errors. I can even rearrange things and get rid of umms or ahs or anything I don't want.Then I use subed-convert to turn it into a VTT file. I can tweak the start and end times by looking at the waveforms. Then I add comments with the visuals I want. I can add images, animated GIFs, or videos, and they're automatically squeezed or stretched to fit. I can also have them play at original speed. Then I set up open captions and use subed-record-compile-video. Tada!
</p>

<p>
Links:
</p>

<ul class="org-ul">
<li><a href="https://www.orgzlyrevived.com/">Orgzly Revived</a></li>
<li><a href="https://play.google.com/store/apps/details?id=com.coffeebeanventures.easyvoicerecorder&amp;hl=en_US">Easy Voice Recorder</a></li>
<li><a href="https://github.com/m-bain/whisperX">WhisperX</a></li>
<li><a href="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/">Using Emacs Lisp to process WhisperX timestamps</a></li>
<li><a href="https://github.com/sachac/subed">Subed</a></li>
<li><a href="https://sachachua.com/blog/category/subed/">My other blog posts about subed</a></li>
<li><a href="https://github.com/sachac/subed-record">Subed-record</a></li>
<li>Animated GIF By DemonDeLuxe (Dominique Toussaint) - Image: Newtons cradle animation book.gif, CC BY-SA 3.0, <a href="https://commons.wikimedia.org/w/index.php?curid=3717500">https://commons.wikimedia.org/w/index.php?curid=3717500</a></li>
</ul>

<p>You can <a href="https://youtube.com/watch?v=https://www.youtube.com/shorts/s6Y_wx-4xsI">watch this on YouTube</a>, <a href="https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/video-workflow.webm">download the video</a>, or <a href="https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/video-workflow.opus">download the audio</a>.</p></div></div>
<div><a href="https://sachachua.com/blog/2024/10/yay-emacs-tweaking-my-video-workflow-with-whisperx-and-subed-record/index.org">View org source for this post</a></div>]]></description>
		</item><item>
		<title>Using WhisperX to get word-level timestamps for audio editing with Emacs and subed-record</title>
		<link>https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 17 Sep 2024 16:34:13 GMT</pubDate>
    <category>emacs</category>
<category>subed</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/</guid>
		<description><![CDATA[<div class="update" id="org594a840">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2024-10-14 Mon]</span></span>: Actually, WhisperX makes a JSON with word-level timing data, so let's use that instead.
</p>

</div>

<p>
I'm gradually shifting more things to this Lenovo
P52 to take advantage of its newer processor, 64
GB of RAM, and 2 TB drive. (Whee!) One of the
things I'm curious about is how I can make better
use of multimedia. I couldn't get <a href="https://github.com/ggerganov/whisper.cpp">whisper.cpp</a> to
work on my Lenovo X230T, so I mostly relied on the
automatic transcripts from Google Recorder (with
timestamps generated by <a href="https://www.readbeyond.it/aeneas/">aeneas</a>) or cloud-based
transcription services like <a href="https://deepgram.com/">Deepgram</a>.
</p>

<p>
I have a lot of silences in my voice notes when I
think out loud. <a href="https://github.com/ggerganov/whisper.cpp">whisper.cpp</a> got stuck in loops
during silent parts, but <a href="https://github.com/m-bain/whisperX">WhisperX</a> handles them
perfectly. WhisperX is also fast enough for me to
handle audio files locally instead of relying on
Deepgram. With the default model, I can process
the files faster than real-time:
</p>

<table>


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

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">File length</th>
<th scope="col" class="org-left">Transcription time</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">42s</td>
<td class="org-left">17s</td>
</tr>

<tr>
<td class="org-left">7m48s</td>
<td class="org-left">1m41s</td>
</tr>
</tbody>
</table>

<p>
I used this command to get word-level timing data.
</p>


<div class="org-src-container">
<pre class="src src-sh">~/vendor/whisperx/.venv/bin/whisperx &#45;&#45;compute_type int8 &#45;&#45;highlight_words True &#45;&#45;print_progress True <span class="org-string">"$1"</span>
</pre>
</div>


<p>
Among other things, it makes a text file that looks like this:
</p>

<pre class="example" id="org82b22d2">
I often need to... I sometimes need to replace or navigate by symbols.
Casual symbol overlays a new package that adds those shortcuts so that I don't have to remember the other keywords for them.
</pre>

<p>
and a JSON file that looks like this:
</p>


<div class="org-src-container">
<pre class="src src-js">{<span class="org-string">"segments"</span>: [{<span class="org-string">"start"</span>: 0.427, <span class="org-string">"end"</span>: 7.751, <span class="org-string">"text"</span>: <span class="org-string">" I often need to... I sometimes need to replace or navigate by symbols."</span>, <span class="org-string">"words"</span>: [{<span class="org-string">"word"</span>: <span class="org-string">"I"</span>, <span class="org-string">"start"</span>: 0.427, <span class="org-string">"end"</span>: 0.507, <span class="org-string">"score"</span>: 0.994}, {<span class="org-string">"word"</span>: <span class="org-string">"often"</span>, <span class="org-string">"start"</span>: 0.587, <span class="org-string">"end"</span>: 0.887, <span class="org-string">"score"</span>: 0.856}, {<span class="org-string">"word"</span>: <span class="org-string">"need"</span>, <span class="org-string">"start"</span>: 0.987, <span class="org-string">"end"</span>: 1.227, <span class="org-string">"score"</span>: 0.851}, {<span class="org-string">"word"</span>: <span class="org-string">"to..."</span>, <span class="org-string">"start"</span>: 1.267, <span class="org-string">"end"</span>: 1.508, <span class="org-string">"score"</span>: 0.738}, {<span class="org-string">"word"</span>: <span class="org-string">"I"</span>, <span class="org-string">"start"</span>: 4.329, <span class="org-string">"end"</span>: 4.429, <span class="org-string">"score"</span>: 0.778}, ...]}, ...]}
</pre>
</div>


<p>
Sometimes I just want the text so that I can use <a href="https://sachachua.com/dotemacs#transcript-keywords">an audio braindump as the starting point for a blog post or for notes</a>. WhisperX is way more accurate than Google Recorder, so that will probably be easier once I update my workflow for that.
</p>

<p>
Sometimes I want to make an edited audio file that sounds smooth so that I can use it in a podcast, a video, or some audio notes. For that, I'd like word-level timing data so that I can cut out words or sections. Aeneas didn't give me word-level timestamps, but WhisperX does, so I can get the time information before I start editing. I can extract the word timestamps from the JSON like this:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-word-tsv-from-whisperx-json</span> (file)
  (<span class="org-keyword">interactive</span> <span class="org-string">"FJSON: "</span>)
  (<span class="org-keyword">let*</span> ((json-array-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">list</span>)
         (json-object-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">alist</span>)
         (data (json-read-file file))
         (filename (concat (file-name-sans-extension file) <span class="org-string">".tsv"</span>))
         (base (seq-mapcat
                (<span class="org-keyword">lambda</span> (segment)
                  (seq-map (<span class="org-keyword">lambda</span> (word)
                             (<span class="org-keyword">let-alist</span> word
                               (list nil
                                     (<span class="org-keyword">and</span> .start (* 1000 .start))
                                     (<span class="org-keyword">and</span> .end (* 1000 .end))
                                     .word)))
                           (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">words</span> segment)))
                (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">segments</span> data)))
         (current base)
         (last-end 0))
     <span class="org-comment-delimiter">;; </span><span class="org-comment">numbers at the end of a sentence sometimes don't end up with times</span>
     <span class="org-comment-delimiter">;; </span><span class="org-comment">so we need to fix them</span>
    (<span class="org-keyword">while</span> current
      (<span class="org-keyword">unless</span> (elt (car current) 1)           <span class="org-comment-delimiter">; </span><span class="org-comment">start</span>
        (<span class="org-keyword">setf</span> (elt (car current) 1) (1+ last-end)))
      (<span class="org-keyword">unless</span> (elt (car current) 2)
        (<span class="org-keyword">setf</span> (elt (car current) 2) (1- (elt (cadr current) 1))))
      (<span class="org-keyword">setq</span>
       last-end (elt (car current) 2)
       current (cdr current)))
    (subed-create-file
     filename
     base
     t
     <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">subed-tsv-mode</span>)
    (find-file filename)))
</pre>
</div>


<p>
Here's my old code for parsing the highlighted VTT or SRT files that underline each word:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-load-word-data-from-whisperx-highlights</span> (file)
  <span class="org-doc">"Return a list of word cues from FILE.</span>
<span class="org-doc">FILE should be a VTT or SRT file produced by whisperx with the</span>
<span class="org-doc">&#45;&#45;highlight_words True option."</span>
  (seq-keep (<span class="org-keyword">lambda</span> (sub)
              (<span class="org-keyword">when</span> (string-match <span class="org-string">"&lt;u&gt;</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">&lt;/u&gt;"</span> (elt sub 3))
                (<span class="org-keyword">setf</span> (elt sub 3) (match-string 1 (elt sub 3)))
                sub))
            (subed-parse-file file)))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-word-tsv-from-whisperx-highlights</span> (file)
  (<span class="org-keyword">interactive</span> <span class="org-string">"FVTT: "</span>)
  (<span class="org-keyword">with-current-buffer</span> (find-file-noselect (concat (file-name-nondirectory file) <span class="org-string">".tsv"</span>))
    (erase-buffer)
    (subed-tsv-mode)
    (subed-auto-insert)
    (mapc (<span class="org-keyword">lambda</span> (sub) (apply <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">subed-append-subtitle</span> nil (cdr sub)))
          (my-subed-load-word-data-from-whisperx-highlights file))
    (switch-to-buffer (current-buffer))))
</pre>
</div>


<p>
I like to use the TSV format for this one because
it's easy to scan down the right side.
Incidentally, this format is compatible with
<a href="https://www.audacityteam.org/">Audacity</a> labels, so I could import that there if I
wanted. I like Emacs much more, though. I'm used
to having all my keyboard shortcuts at hand.
</p>

<pre class="example" id="org9f771a4">
0.427000	0.507000	I
0.587000	0.887000	often
0.987000	1.227000	need
1.267000	1.508000	to...
4.329000	4.429000	I
4.469000	4.869000	sometimes
4.950000	5.170000	need
5.210000	5.410000	to
5.530000	6.090000	replace
</pre>

<p>
Once I've deleted the words I don't want to
include, I can merge subtitles for phrases so that
I can keep the pauses between words. A quick
heuristic is to merge subtitles if they don't have
much of a pause between them.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-subed-merge-close-subtitles-threshold</span> 500)
(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-merge-close-subtitles</span> (threshold)
  <span class="org-doc">"Merge subtitles with the following one if there is less than THRESHOLD msecs gap between them."</span>
  (<span class="org-keyword">interactive</span> (list (read-number <span class="org-string">"Threshold in msecs: "</span> my-subed-merge-close-subtitles-threshold)))
  (goto-char (point-min))
  (<span class="org-keyword">while</span> (not (eobp))
    (<span class="org-keyword">let</span> ((end (subed-subtitle-msecs-stop))
          (next-start (<span class="org-keyword">save-excursion</span>
                        (<span class="org-keyword">and</span> (subed-forward-subtitle-time-start)
                             (subed-subtitle-msecs-stop)))))
      (<span class="org-keyword">if</span> (<span class="org-keyword">and</span> end next-start (&lt; (- next-start end) threshold))
          (subed-merge-with-next)
        (<span class="org-keyword">or</span> (subed-forward-subtitle-end) (goto-char (point-max)))))))
</pre>
</div>


<p>
Then I can use <code>subed-waveform-show-all</code> to tweak the start and end timestamps.
Here I switch to another file I've been editing&#x2026;
</p>


<figure id="orgc343a64">
<img src="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/2024-09-17-12-06-12.svg" alt="2024-09-17-12-06-12.svg" class="org-svg" data-link="t">

<figcaption><span class="figure-number">Figure 1: </span>Screenshot of subed-waveform</figcaption>
</figure>

<p>
After that, I can <a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/">use subed-record to compile the
audio</a> into an <code>.opus</code> file that sounds reasonably smooth.
</p>

<p>
</p><div class="audio"><audio controls="1" preload="metadata" src="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/casual-symbol-overlay.opus" type="audio/ogg"><a href="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/casual-symbol-overlay.opus">Download the audio</a><track kind="captions" label="Captions" src="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/casual-symbol-overlay.vtt" srclang="en" default=""></audio></div>
<p></p>

<span class="audio-time" data-start="0.000000" data-end="2.999000">I sometimes need to replace or navigate by symbols.</span>
<span class="audio-time" data-start="3.000000" data-end="4.339000"><a href="http://yummymelon.com/devnull/announcing-casual-symbol-overlay.html">casual-symbol-overlay</a></span>
<span class="audio-time" data-start="4.340000" data-end="5.684000">is a package that adds a</span>
<span class="audio-time" data-start="5.685000" data-end="9.279000">transient menu so that I don't have to remember the keyboard shortcuts for them.</span>
<span class="audio-time" data-start="9.280000" data-end="10.371000">I've added it to my</span>
<span class="audio-time" data-start="10.372000" data-end="11.612000">embark-symbol-keymap</span>
<span class="audio-time" data-start="11.613000" data-end="13.592000">so I can call it with embark-act.</span>
<span class="audio-time" data-start="13.593000" data-end="16.932000">That way it's just a C-. z away.</span>

<p>
I want to make lots of quick audio notes that I
can shuffle and listen to in order to remember
things I'm learning about Emacs (might even come
up with some kind of spaced repetition system),
and I'd like to make more videos someday too. I
think WhisperX, subed, and Org Mode will be fun
parts of my workflow.</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#whisperx">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/index.org">View org source for this post</a></div>]]></description>
		</item><item>
		<title>EmacsConf backstage: making lots of intro videos with subed-record</title>
		<link>https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 05 Jan 2024 13:02:09 GMT</pubDate>
    <category>emacsconf</category>
<category>subed</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/</guid>
		<description><![CDATA[<summary id="orgccd1ac4">
<p>
Summary (735 words): Emacs is a handy audio/video editor. subed-record
can combine multiple audio files and images to create multiple output
videos.
</p>
</summary>

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

<p>
Yay Emacs!
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/emacsconf-backstage-making-lots-of-intro-videos-with-subed-record/index.org">View org source for this post</a></div>]]></description>
		</item><item>
		<title>Using subed-record in Emacs to edit audio and clean up oopses</title>
		<link>https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 29 Dec 2023 15:34:39 GMT</pubDate>
    <category>emacs</category>
<category>subed</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/</guid>
		<description><![CDATA[<p>
Finding enough quiet focused time to record audio is a challenge. I
often have to re-record segments in order to correct brain hiccups or
to restart after interruptions. It's also hard for me to sit still and
listen to my recordings looking for mistakes to edit out. I'm not
familiar enough with Audacity to zip around with keyboard shortcuts,
and I don't like listening to myself again and again in order to find
my way around an audio file.
</p>

<p>
Sure, I could take the transcript, align it with <code>subed-align</code> and
Aeneas to get the timestamps, and then use <code>subed-convert</code> to get a CSV (actually a TSV since it uses tabs)
that I can import into Audacity as labels, but it still feels a little
awkward to navigate. I have to zoom in a lot for the text to be
readable.
</p>


<figure id="org0547043">
<img src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_10-28-32.png" alt="2023-12-29_10-28-32.png">

<figcaption><span class="figure-number">Figure 1: </span>Audacity labels</figcaption>
</figure>

<p>
So here's a workflow I've been experimenting with for cleaning up my
recorded audio.
</p>

<p>
Just like with <a href="https://sachachua.com/blog/2023/12/audio-braindump-workflow-tweaks-adding-org-mode-hyperlinks-to-recordings-based-on-keywords/">my audio braindumps</a>, I use Google Recorder on my phone
because I can get the audio file and a rough transcript, and because
the microphone on it is better than on my laptop. For narration
recordings, I hide in the closet because the clothes muffle echoes. I
don't feel as self-conscious there as I might be if I recorded in the
kitchen, where my computer usually is. I used to record in Emacs using
<code>subed-record</code> by pressing <code>left</code> to redo a segment and <code>right</code> to
move on to the next one, but using my phone means I don't have to deal
with the computer's noises or get the good mic from downstairs.
</p>

<p>
I start the recorder on my phone and then switch to my Org file in
Orgzly Revived, where I've added my script. I read it
as far as I can go. If I want to redo a segment, I say "Oops" and then
just redo the last phrase or so.
</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>Screenshot of Google Recorder on my phone</strong></summary>

<figure id="org0620e12">
<img src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/Screenshot_20231229-083047.png" alt="Screenshot_20231229-083047.png">

</figure>


</details>

<p>
I export the transcript and the M4A audio file using Syncthing, which
copies them to my computer. I have a function that copies the latest
recording and even sets things up for removing oops segments
(<code>my-subed-copy-latest-phone-recording</code>, which calls <code>my-split-oops</code>).
If I want to process several files, I can copy them over with
<code>my-subed-copy-recording</code>.
</p>

<p>
</p><details><summary>my-subed-copy-latest-phone-recording: Copy the latest recording transcript and audio to DESTINATION.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-copy-latest-phone-recording</span> (destination)
  <span class="org-doc">"Copy the latest recording transcript and audio to DESTINATION."</span>
  (<span class="org-keyword">interactive</span>
   (list
    (file-name-directory
     (read-file-name (format <span class="org-string">"Move %s to: "</span>
                             (file-name-base (my-latest-file my-phone-recording-dir <span class="org-string">".txt"</span>)))
                     nil nil nil nil <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">file-directory-p</span>))))
  (<span class="org-keyword">let</span> ((base (file-name-base (my-latest-file my-phone-recording-dir <span class="org-string">".txt"</span>))))
    (rename-file (expand-file-name (concat base <span class="org-string">".txt"</span>) my-phone-recording-dir)
                 destination)
    (rename-file (expand-file-name (concat base <span class="org-string">".m4a"</span>) my-phone-recording-dir)
                 destination)
    (find-file (expand-file-name (concat base <span class="org-string">".txt"</span>) destination))
    (<span class="org-keyword">save-excursion</span> (my-split-oops))
    (goto-char (point-min))
    (flush-lines <span class="org-string">"^$"</span>)
    (goto-char (point-min))
    (subed-forward-subtitle-id)
    (subed-set-subtitle-comment
     (concat <span class="org-string">"#+OUTPUT: "</span>
             (file-name-base (buffer-file-name))
             <span class="org-string">"-cleaned.opus"</span>))))
</pre></div></details>
<p></p>

<p>
</p><details><summary>my-subed-copy-recording</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-subed-copy-recording</span> (filename destination)
  (<span class="org-keyword">interactive</span>
   (list
    (buffer-file-name)
    (file-name-directory
     (read-file-name (format <span class="org-string">"Copy %s to: "</span>
                             (file-name-base (buffer-file-name)))
                     nil nil nil nil <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">file-directory-p</span>))))
  (<span class="org-keyword">dolist</span> (ext <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"m4a"</span> <span class="org-string">"txt"</span> <span class="org-string">"json"</span> <span class="org-string">"vtt"</span>))
    (<span class="org-keyword">when</span> (file-exists-p (concat (file-name-sans-extension filename) <span class="org-string">"."</span> ext))
      (copy-file (concat (file-name-sans-extension filename) <span class="org-string">"."</span> ext)
                 destination t)))
  (<span class="org-keyword">when</span> (get-file-buffer filename)
    (kill-buffer (get-file-buffer filename))
    (dired destination)))
</pre></div></details>
<p></p>

<p>
I'll use <a href="https://www.readbeyond.it/aeneas/">Aeneas</a> to get the timestamps for each line of text, so a
little bit of text processing will let me identify the segments that I
want to remove. The way <code>my-split-oops</code> works is that it looks for
"oops" in the transcript. Whenever it finds "oops", it adds a newline
afterwards. Then it takes the next five words and sees if it can
search backward for them within 300 characters. If it finds the words,
then that's the start of my repeated segment, and we can add a newline
before that. If it doesn't find the words, we try again with four
words, then three, then two, then one. I can also manually review the
file and see if the oopses are well lined up. When they're detected
properly, I should see partially duplicated lines.
</p>

<pre style="word-wrap: whitespace">
I used to record using sub-record by using by. Oops,
I used to record. Oops,
I used to record an emacs using subhead record, by pressing left to reduce segment, and write to move on to the next one.
But using my phone means, I don't have to deal with them. Oops.
But using my phone means, I don't have to deal with the computer's noises or get the good mic from downstairs. I started recorder on my phone
</pre>

<p>
</p><details><summary>my-split-oops: Look for oops and make it easier to split.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-split-oops</span> ()
  <span class="org-doc">"Look for oops and make it easier to split."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">let</span> ((scan-window 300))
    (<span class="org-keyword">while</span> (re-search-forward <span class="org-string">"oops[,</span><span class="org-string"><span class="org-warning">\</span></span><span class="org-string">.]?[ \n]+"</span> nil t)
      (<span class="org-keyword">let</span> ((start (min (line-beginning-position) (- (point) scan-window)))
            start-search
            found
            search-for)
        (<span class="org-keyword">if</span> (bolp)
            (<span class="org-keyword">progn</span>
              (backward-char)
              (<span class="org-keyword">setq</span> start (min (line-beginning-position) (- (point) scan-window))))
          (insert <span class="org-string">"\n"</span>))
        (<span class="org-keyword">save-excursion</span>
          (<span class="org-keyword">setq</span> start-search (point))
          <span class="org-comment-delimiter">;; </span><span class="org-comment">look for 1..3 words back</span>
          (goto-char
           (<span class="org-keyword">or</span>
            (<span class="org-keyword">cl-loop</span>
             for n downfrom 4 downto 1
             do
             (<span class="org-keyword">save-excursion</span>
               (<span class="org-keyword">dotimes</span> (_ n) (forward-word))
               (<span class="org-keyword">setq</span> search-for (downcase (string-trim (buffer-substring start-search (point)))))
               (goto-char start-search)
               (<span class="org-keyword">when</span> (re-search-backward (regexp-quote search-for) start t)
                 (goto-char (match-beginning 0))
                 (<span class="org-keyword">cl-return</span> (point)))))
            (<span class="org-keyword">and</span> (call-interactively <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">isearch-backward</span>) (point))))
          (insert <span class="org-string">"\n"</span>))))))
</pre></div></details>
<p></p>

<p>
Once the lines are split up, I use <code>subed-align</code> and get a VTT file.
The oops segments will be in their own subtitles.
</p>


<figure id="org4821b71">
<img src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29-08-41-33.svg" alt="2023-12-29-08-41-33.svg" class="org-svg">

<figcaption><span class="figure-number">Figure 2: </span>Subtitles and waveforms</figcaption>
</figure>

<p>
The timestamps still need a bit of tweaking sometimes, so I use
<code>subed-waveform-show-current</code> or <code>subed-waveform-show-all</code>. I can use
the following bindings:
</p>

<ul class="org-ul">
<li>middle-click to play a sample</li>
<li>M-left-click to set the start and copy to the previous subtitle</li>
<li>left-click to set the start without changing the previous one</li>
<li>M-right-click to set the end and copy to the next subtitle</li>
<li>right-click to set the end without changing the next one</li>
<li>M-j to jump to the current subtitle and play it again in MPV</li>
<li>M-J to jump to close to the end of the current subtitle and play it in MPV</li>
</ul>

<p>
<video controls=""><source src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_08.43.27.webm" type="video/webm"><a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_08.43.27.webm">Download the video</a></video>
</p>

<p>
I use <code>my-subed-delete-oops</code> to delete the oops segments. I can also
just mark them for skipping by calling <code>C-u M-x my-subed-delete-oops</code>
instead.
</p>

<p>
Then I add a <code>#+OUTPUT: filename-cleaned.opus</code> comment under a <code>NOTE</code>
near the beginning of the file. This tells
<code>subed-record~compile-audio</code> where to put the output.
</p>


<div class="org-src-container">
<pre class="src src-subed-vtt">WEBVTT

<span class="org-comment">NOTE #+SKIP</span>

<span class="org-subed-time">00:00:00.000</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:10.319</span>
Finding enough. Oops.

<span class="org-comment">NOTE</span>
<span class="org-comment">#+OUTPUT: 2023-12-subed-record-cleaned.opus</span>

<span class="org-subed-time">00:00:10.320</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:00:36.319</span>
Finding enough quiet Focused. Time to record. Audio is a challenge. I often have to re-record segments in order to correct brain hiccups, or to restart after interruptions.
</pre>
</div>


<p>
I can test short segments by marking the region with <code>C-SPC</code> and using
<code>subed-record-compile-try-flow</code>. This lets me check if the transitions
between segments make sense.
</p>

<p>
<video controls=""><source src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_09.27.53.webm" type="video/webm"><a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_09.27.53.webm">Download the video</a></video>
</p>

<p>
When I'm happy with everything, I can use <code>subed-record-compile-audio</code>
to extract the segments specified by the start and end times of each
subtitle and concatenate them one after the other in the audio file
specified by the output. The result should be a clean audio file.
</p>

<p>
<audio controls="" src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-subed-record-cleaned.opus" type="audio/ogg"><a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-subed-record-cleaned.opus">Download the audio</a></audio>
</p>

<p>
If I need to compile an audio file from several takes, I process each
take separately. Once I've adjusted the timestamps and deleted or
skipped the oops segments, I add <code>#+AUDIO: input-filename.opus</code> to a
<code>NOTE</code> at the beginning of the file.
<code>subed-record-insert-audio-source-note</code> makes this easier. Then I copy
the file's subtitles into my main file. <code>subed-record-compile-audio</code> will take
the audio from whichever file was specified by the <code>#+AUDIO:</code> comment,
so I can use audio from different files.
</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>Example VTT segment with multiple audio files</strong></summary>

<div class="org-src-container">
<pre class="src src-subed-vtt"><span class="org-comment">NOTE</span>
<span class="org-comment">#+AUDIO: 2023-11-11-emacsconf.m4a</span>

<span class="org-subed-time">00:10:55.617</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:10:58.136</span>
Sometimes we send emails one at a time.

<span class="org-comment">NOTE</span>
<span class="org-comment">#+AUDIO: 2023-11-15-emacsconf.m4a</span>

<span class="org-subed-time">00:10:55.625</span> <span class="org-subed-time-separator">&#45;&#45;&gt;</span> <span class="org-subed-time">00:11:03.539</span>
Like when you let a speaker know that we've received a proposal That's mostly a matter of plugging the talks properties into the right places in the template.
</pre>
</div>



</details>

<p>
Now I have a clean audio file that corresponds to my script. I can use
<code>subed-align</code> on my script to get the timestamps for each line using
the cleaned audio. Once I have a subtitle file, I can use
<code>emacsconf-subed-split</code> (in <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-subed.el">emacsconf-subed.el</a> - which I
probably should add to <code>subed-mode</code> sometime) to quickly split the
captions up to fit the line lengths. Then I redo the timestamps with
<code>subed-align</code> and adjust timestamps with
<code>subed-waveform-show-current</code>.
</p>

<p>
<video controls=""><source src="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_10.22.31.webm" type="video/webm"><a href="https://sachachua.com/blog/2023/12/using-subed-record-in-emacs-to-edit-audio-and-clean-up-oopses/2023-12-29_10.22.31.webm">Download the video</a></video>
</p>

<p>
So that's how I go from rough recordings with stutters and oopses to a
clean audio file with captions based on my script. People can probably
edit faster with Audacity wizardry or the AI audio editors that are in
vogue these days, but this little workflow gets around my impatience
with audio by turning it into (mostly) text, so that's cool. Let's see
if I can make more presentations now that I've gotten the audio side
figured out!
</p>

<p>
Links:
</p>

<ul class="org-ul">
<li><a href="https://github.com/sachac/subed">https://github.com/sachac/subed</a></li>
<li><a href="https://github.com/sachac/subed-record">https://github.com/sachac/subed-record</a></li>
<li><a href="https://github.com/sachac/compile-media">https://github.com/sachac/compile-media</a> (which subed-record uses to make ffmpeg commands)</li>
<li><a href="https://readbeyond.it/aeneas">https://readbeyond.it/aeneas</a> - forced alignment tool</li>
</ul>
]]></description>
		</item><item>
		<title>#EmacsConf backstage: autopilot with crontab</title>
		<link>https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 24 Oct 2023 14:43:35 GMT</pubDate>
    <category>emacs</category>
<category>emacsconf</category>
<category>subed</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/</guid>
		<description><![CDATA[<div class="update" id="org2c632f7">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2023-10-26 Thu]</span></span>: updated handle-session and added talk
</p>

</div>

<p>
I figured out multi-track streaming so close to EmacsConf 2022 that
there wasn't enough time to get other volunteers used to working with
the setup, especially since I was still scrambling to figure out more
infrastructure as the conference approached. We decided I'd run both
streams myself, which meant I needed to make things as automatic as
possible so that I wouldn't go crazy. I wanted a lot of things to
happen automatically: playing recorded intros and videos, browsing to
the right URLs depending on the type of Q&amp;A, publishing updates to the
wiki, and so on.
</p>

<p>
I used <a href="https://sachachua.com/blog/2023/01/emacsconf-backstage-using-tramp-and-timers-to-run-two-tracks-semi-automatically/">timers and TODO state changes to execute commands via TRAMP</a>,
which was pretty cool for the most part. But it turned out TRAMP
doesn't like being called when it's already running, like when it's
being called from two timers going off at the same time. It gives a
"Forbidden reentrant call of TRAMP". We found a couple of quick
workarounds: I could reschedule the talks to be a minute apart, or I
could cancel the conflicting timer and just start them with the shell
scripts.
</p>

<p>
Last year, we had a shell script that played the intro and the main
talk, and other scripts to handle the Q&amp;A by opening BigBlueButton,
Etherpad, or the IRC channel. Much of the logic was in Emacs Lisp
because it was easy to write it that way. For this year, I wanted to
write a script that handled the intro, video, and Q&amp;A portions. This
is now in <a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/obs/templates/handle-session">roles/obs/templates/handle-session</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>handle-session</strong></summary>
<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span>
<span class="org-comment-delimiter"># </span><span class="org-comment"></span>
<span class="org-comment-delimiter">#</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">Handle the intro/talk/Q&amp;A for a session</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">Usage: handle-session $SLUG</span>

<span class="org-variable-name">YEAR</span>=<span class="org-string">""</span>
<span class="org-variable-name">BASE_DIR</span>=<span class="org-string">""</span>
<span class="org-variable-name">FIREFOX_NAME</span>=firefox-esr
<span class="org-variable-name">SLUG</span>=$<span class="org-variable-name">1</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Kill background music if playing</span>
<span class="org-keyword">if</span> screen -list | grep -q background; <span class="org-keyword">then</span>
    screen -S background -X quit
<span class="org-keyword">fi</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Update the status</span>
sudo -u  talk $<span class="org-variable-name">SLUG</span> PLAYING &amp;

<span class="org-comment-delimiter"># </span><span class="org-comment">Update the overlay</span>
overlay $<span class="org-variable-name">SLUG</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Play the intro if it exists. If it doesn't exist, switch to the intro slide and stop processing.</span>

<span class="org-keyword">if</span> [[ -f $<span class="org-variable-name">BASE_DIR</span>/assets/intros/$<span class="org-variable-name">SLUG</span>.webm ]]; <span class="org-keyword">then</span>
  killall -s TERM $<span class="org-variable-name">FIREFOX_NAME</span>
  mpv $<span class="org-variable-name">BASE_DIR</span>/assets/intros/$<span class="org-variable-name">SLUG</span>.webm
<span class="org-keyword">else</span>
  firefox &#45;&#45;kiosk $<span class="org-variable-name">BASE_DIR</span>/assets/in-between/$<span class="org-variable-name">SLUG</span>.png
  <span class="org-keyword">exit</span> 0
<span class="org-keyword">fi</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Play the video if it exists. If it doesn't exist, switch to the BBB room and stop processing.</span>
<span class="org-keyword">if</span> [ <span class="org-string">"x$TEST_MODE"</span> = <span class="org-string">"x"</span> ]; <span class="org-keyword">then</span>
  <span class="org-variable-name">LIST</span>=($<span class="org-variable-name">BASE_DIR</span>/assets/stream/&#45;&#45;$<span class="org-variable-name">SLUG</span>*&#45;&#45;main.webm)
<span class="org-keyword">else</span>
  <span class="org-variable-name">LIST</span>=($<span class="org-variable-name">BASE_DIR</span>/assets/test/&#45;&#45;$<span class="org-variable-name">SLUG</span>*&#45;&#45;main.webm)
<span class="org-keyword">fi</span>
<span class="org-variable-name">FILE</span>=<span class="org-string">"${LIST[0]}"</span>
<span class="org-keyword">if</span> [ <span class="org-negation-char">!</span> -f <span class="org-string">"$FILE"</span> ]; <span class="org-keyword">then</span>
    <span class="org-comment-delimiter"># </span><span class="org-comment">Is there an original file?</span>
    <span class="org-variable-name">LIST</span>=($<span class="org-variable-name">BASE_DIR</span>/assets/stream/&#45;&#45;$<span class="org-variable-name">SLUG</span>*&#45;&#45;original.{webm,mp4,mov})
    <span class="org-variable-name">FILE</span>=<span class="org-string">"${LIST[0]}"</span>
<span class="org-keyword">fi</span>

<span class="org-keyword">if</span> [[ -f $<span class="org-variable-name">FILE</span> ]]; <span class="org-keyword">then</span>
  killall -s TERM $<span class="org-variable-name">FIREFOX_NAME</span>
  mpv $<span class="org-variable-name">FILE</span>
<span class="org-keyword">else</span>
  /usr/local/bin/bbb $<span class="org-variable-name">SLUG</span>
  <span class="org-keyword">exit</span> 0
<span class="org-keyword">fi</span>

sudo -u  talk $<span class="org-variable-name">SLUG</span> CLOSED_Q &amp;

<span class="org-comment-delimiter"># </span><span class="org-comment">Open the appropriate Q&amp;A URL</span>
<span class="org-variable-name">QA</span>=$(jq -r <span class="org-string">'.talks[] | select(.slug=="'</span>$<span class="org-variable-name">SLUG</span><span class="org-string">'")["qa-backstage-url"]'</span> &lt; $<span class="org-variable-name">BASE_DIR</span>/talks.json)
<span class="org-variable-name">QA_TYPE</span>=$(jq -r <span class="org-string">'.talks[] | select(.slug=="'</span>$<span class="org-variable-name">SLUG</span><span class="org-string">'")["qa-type"]'</span> &lt; $<span class="org-variable-name">BASE_DIR</span>/talks.json)
<span class="org-builtin">echo</span> <span class="org-string">"QA_TYPE $QA_TYPE QA $QA"</span>
<span class="org-keyword">if</span> [ <span class="org-string">"$QA_TYPE"</span> = <span class="org-string">"live"</span> ]; <span class="org-keyword">then</span>
  /usr/local/bin/bbb $<span class="org-variable-name">SLUG</span>
<span class="org-keyword">elif</span> [ <span class="org-string">"$QA"</span> != <span class="org-string">"null"</span> ]; <span class="org-keyword">then</span>
  /usr/local/bin/music &amp;
  /usr/bin/firefox $<span class="org-variable-name">QA</span>
  <span class="org-comment-delimiter"># </span><span class="org-comment">i3-msg 'layout splith'</span>
<span class="org-keyword">fi</span>
<span class="org-builtin">wait</span>
</pre>
</div>


</details>

<p>
It builds on <a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/obs/templates/bbb">roles/obs/templates/bbb</a>,
<a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/obs/templates/overlay">roles/obs/templates/overlay</a>, and
<a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/obs/templates/music">roles/obs/templates/music</a>. I also have a
<a href="https://git.emacsconf.org/emacsconf-ansible/tree/roles/prerec/templates/talk">roles/prerec/templates/talk</a> script that uses
emacsclient to update the status of the talk.
</p>

<p>
I wrote some Tampermonkey scripts to automate <a href="https://sachachua.com/blog/2023/10/emacsconf-backstage-automatically-joining-bigbluebutton-web-conferences-using-tampermonkey/">joining the web
conference and the IRC channel</a>.
</p>

<p>
Now that we have a script that handles all the different things
related to a session, it's easier to schedule the execution of that
script. Instead of using Emacs timers and running into that problem
with tramp, I want to try using cron. Cron is a standard UNIX and
Linux tool for scheduling things to run at certain times. You make a
plain text file in a particular format: minute, hour, day of month,
month, day of week, and then the command, and then you tell cron to
use that file with something like <code>crontab your-file</code>. Since it's
plain text, we can generate it with Emacs Lisp and
<code>format-time-string</code>, save with TRAMP, and install with <code>ssh</code>. Each
track has its own user account for streaming, so each track can have
its own file.
</p>

<p>
</p><details open=""><summary>emacsconf-stream-format-crontab: Return crontab entries for TALKS.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-format-crontab</span> (track talks <span class="org-type">&amp;optional</span> test-mode)
  <span class="org-doc">"Return crontab entries for TALKS.</span>
<span class="org-doc">Use the display specified in TRACK.</span>
<span class="org-doc">If TEST-MODE is non-nil, load the videos from the test directory."</span>
  (concat
   (format
    <span class="org-string">"PATH=/usr/local/bin:/usr/bin</span>
<span class="org-string">MAILTO=\"\"</span>
<span class="org-string">XDG_RUNTIME_DIR=\"/run/user/%d\"</span>
<span class="org-string">"</span> (plist-get track <span class="org-builtin">:uid</span>))
   (mapconcat
    (<span class="org-keyword">lambda</span> (talk)
      (format <span class="org-string">"%s /usr/bin/screen -dmS play-%s bash -c \"DISPLAY=%s TEST_MODE=%s /usr/local/bin/handle-session %s | tee -a ~/track.log\"\n"</span>
              <span class="org-comment-delimiter">;; </span><span class="org-comment">cron times are UTC</span>
              (format-time-string <span class="org-string">"%-M %-H %-d %m *"</span> (plist-get talk <span class="org-builtin">:start-time</span>))
              (plist-get talk <span class="org-builtin">:slug</span>)
              (plist-get track <span class="org-builtin">:vnc-display</span>)
              (<span class="org-keyword">if</span> test-mode <span class="org-string">"1"</span> <span class="org-string">""</span>)
              (plist-get talk <span class="org-builtin">:slug</span>)))
    (emacsconf-filter-talks talks))))
</pre></div></details>
<p></p>

<p>
</p><details open=""><summary>emacsconf-stream-crontabs: Write the streaming users’ crontab files.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-crontabs</span> (<span class="org-type">&amp;optional</span> test-mode info)
  <span class="org-doc">"Write the streaming users' crontab files.</span>
<span class="org-doc">If TEST-MODE is non-nil, use the videos in the test directory.</span>
<span class="org-doc">If INFO is non-nil, use that as the schedule instead."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">let</span> ((emacsconf-publishing-phase <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">conference</span>))
    (<span class="org-keyword">setq</span> info (<span class="org-keyword">or</span> info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
    (<span class="org-keyword">dolist</span> (track emacsconf-tracks)
      (<span class="org-keyword">let</span> ((talks (seq-filter (<span class="org-keyword">lambda</span> (talk)
                                 (string= (plist-get talk <span class="org-builtin">:track</span>)
                                          (plist-get track <span class="org-builtin">:name</span>)))
                               info))
            (crontab (expand-file-name (concat (plist-get track <span class="org-builtin">:id</span>) <span class="org-string">".crontab"</span>)
                                       (concat (plist-get track <span class="org-builtin">:tramp</span>) <span class="org-string">"~"</span>))))
        (<span class="org-keyword">with-temp-file</span> crontab
          (<span class="org-keyword">when</span> (plist-get track <span class="org-builtin">:autopilot</span>)
            (insert (emacsconf-stream-format-crontab track talks test-mode))))
        (emacsconf-stream-track-ssh track (concat <span class="org-string">"crontab ~/"</span> (plist-get track <span class="org-builtin">:id</span>) <span class="org-string">".crontab"</span>))))))
</pre></div></details>
<p></p>

<p>
I want to test the whole setup before the conference, of course.
First, I needed test videos. This generates test videos and subtitles
following our naming convention.
</p>

<p>
</p><details open=""><summary>emacsconf-stream-generate-test-videos</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-generate-test-videos</span> (<span class="org-type">&amp;optional</span> info)
  <span class="org-doc">"Generate 1-minute test videos for INFO."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">setq</span> info (<span class="org-keyword">or</span> info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
  (<span class="org-keyword">let*</span> ((dir (expand-file-name <span class="org-string">"test"</span> emacsconf-stream-asset-dir))
         (default-directory dir)
         (subed-default-subtitle-length 1000)
         (test-length 60))
    (<span class="org-keyword">unless</span> (file-directory-p dir)
      (make-directory dir t))
    (shell-command
     (format <span class="org-string">"ffmpeg -y -f lavfi -i testsrc=duration=%d:size=1280x720:rate=10 -i background-music.opus -shortest %s "</span>
             test-length (expand-file-name <span class="org-string">"template.webm"</span> dir)))
    (<span class="org-keyword">dolist</span> (talk info)
      (<span class="org-keyword">with-temp-file</span> (expand-file-name (concat (plist-get talk <span class="org-builtin">:file-prefix</span>) <span class="org-string">"&#45;&#45;main.vtt"</span>) dir)
        (subed-vtt-mode)
        (subed-auto-insert)
        (<span class="org-keyword">dotimes</span> (i test-length)
          (subed-append-subtitle
           nil
           (* i 1000)
           (1- (* i 1000))
           (format <span class="org-string">"%s %02d %s"</span>
                   (plist-get talk <span class="org-builtin">:slug</span>)
                   i
                   (substring <span class="org-string">"123456789 123456789 123456789 123456789 123456789 123456789 "</span>
                              (1+ (length (format <span class="org-string">"%s %02d"</span> (plist-get talk <span class="org-builtin">:slug</span>) i))))))))
      (copy-file
       (expand-file-name <span class="org-string">"template.webm"</span> dir)
       (expand-file-name (concat (plist-get talk <span class="org-builtin">:file-prefix</span>) <span class="org-string">"&#45;&#45;main.webm"</span>) dir)
       t))))
</pre></div></details>
<p></p>

<p>
Then I needed to write a crontab based on a different schedule. This
code sets up a series of test videos to start about a minute after I
run the code, with the dev stream set up to start a minute after the
gen stream.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let*</span> ((offset-seconds 60)
       (start-time (time-add (current-time) offset-seconds))
       (emacsconf-schedule-validation-functions nil)
       (emacsconf-schedule-default-buffer-minutes 1)
       (emacsconf-schedule-default-buffer-minutes-for-live-q-and-a 1)
       (emacsconf-schedule-strategies <span class="org-highlight-quoted-quote">'</span>(emacsconf-schedule-allocate-buffer-time
                                        emacsconf-schedule-copy-previous-track))
       (schedule (emacsconf-schedule-prepare
                  (emacsconf-schedule-inflate-sexp
                   <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"GEN"</span>
                      <span class="org-builtin">:start</span> ,(format-time-string <span class="org-string">"%Y-%m-%d %H:%M"</span> start-time)
                      <span class="org-builtin">:set-track</span> <span class="org-string">"General"</span>)
                     (sat-open <span class="org-builtin">:time</span> 1)
                     (uni <span class="org-builtin">:time</span> 1) <span class="org-comment-delimiter">; </span><span class="org-comment">live Q&amp;A</span>
                     (adventure <span class="org-builtin">:time</span> 1) <span class="org-comment-delimiter">; </span><span class="org-comment">pad Q&amp;A</span>
                     (<span class="org-string">"DEV"</span>
                      <span class="org-builtin">:start</span>
                      ,(format-time-string <span class="org-string">"%Y-%m-%d %H:%M"</span> (time-add start-time 60))
                      <span class="org-builtin">:set-track</span> <span class="org-string">"Development"</span>)
                     (repl <span class="org-builtin">:time</span> 1) <span class="org-comment-delimiter">; </span><span class="org-comment">IRC</span>
                     (matplotllm <span class="org-builtin">:time</span> 1) <span class="org-comment-delimiter">; </span><span class="org-comment">pad</span>
                     (voice <span class="org-builtin">:time</span> 1) <span class="org-comment-delimiter">; </span><span class="org-comment">live</span>
                     )))))
  (emacsconf-stream-crontabs t schedule))
</pre>
</div>

<p>
That generates gen.crontab and dev.crontab. This is what gen.crontab looks like for testing:
</p>

<div class="org-src-container">
<pre class="src src-example">PATH=/usr/local/bin:/usr/bin
MAILTO=""
XDG_RUNTIME_DIR="/run/user/2002"
35 11 26 10 * /usr/bin/screen -dmS play-sat-open bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session sat-open | tee -a ~/track.log"
36 11 26 10 * /usr/bin/screen -dmS play-uni bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session uni | tee -a ~/track.log"
38 11 26 10 * /usr/bin/screen -dmS play-adventure bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session adventure | tee -a ~/track.log"
</pre>
</div>

<p>
The result: for both tracks, the intro videos play, the test videos
play, and web browsers go to the right places for the Q&amp;A.
</p>

<p>
<video controls=""><source src="https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/2023-10-24 Crontabs-compressed.webm" type="video/webm"><a href="https://sachachua.com/blog/2023/10/emacsconf-backstage-autopilot-with-crontab/2023-10-24 Crontabs-compressed.webm">Download the video</a></video>
</p>

<p>
In case I need to resume manual control:
</p>

<p>
</p><details open=""><summary>emacsconf-stream-cancel-crontab: Remove crontab for TRACK.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-cancel-crontab</span> (track)
  <span class="org-doc">"Remove crontab for TRACK."</span>
  (<span class="org-keyword">interactive</span> (list (emacsconf-complete-track)))
  (plist-put track <span class="org-builtin">:autopilot</span> nil)
  (emacsconf-stream-track-ssh track <span class="org-string">"crontab -r"</span>))
</pre></div></details>
<p></p>

<p>
</p><details open=""><summary>emacsconf-stream-cancel-all-crontabs: Remove crontabs.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-stream-cancel-all-crontabs</span> ()
  <span class="org-doc">"Remove crontabs."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">dolist</span> (track emacsconf-tracks)
    (plist-put track <span class="org-builtin">:autopilot</span> nil)
    (emacsconf-stream-track-ssh track <span class="org-string">"crontab -r"</span>)))
</pre></div></details>
<p></p>

<p>
Here are some things I learned along the way:
</p>

<ul class="org-ul">
<li><p>
I needed to use <code>timedatectl set-timezone America/Toronto</code> to change
the server's timezone to America/Toronto so that the crontab would
run at the right time.
</p>

<p>
In Ansible terms, that's:
</p>

<div class="org-src-container">
<pre class="src src-ansible">	- name: Set system timezone
		tags: tz
		community.general.timezone:
			name: ""
	- name: Restart cron
		tags: tz
		ansible.builtin.service:
			name: cron
			state: restarted
</pre>
</div></li>

<li>I also needed to specify the <code>PATH</code> so that I didn't need to add the
absolute paths in all the other shell scripts, <code>XDG_RUNTIME_DIR</code> to
get audio working, and <code>DISPLAY</code> so that windows showed up in the
right place.</li>
</ul>

<p>
I think this will let me run both tracks for EmacsConf with more ease
and less frantic juggling. We'll see!
</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>
	</channel>
</rss>