<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://metriccaution.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://metriccaution.github.io//" rel="alternate" type="text/html" /><updated>2026-06-22T17:20:37+01:00</updated><id>https://metriccaution.github.io//feed.xml</id><title type="html">metriccaution</title><subtitle>General thoughts about things.</subtitle><author><name>David McGrath</name></author><entry><title type="html">Rules Cheat-Sheets</title><link href="https://metriccaution.github.io//2026/06/17/rules-cheatsheet/" rel="alternate" type="text/html" title="Rules Cheat-Sheets" /><published>2026-06-17T00:00:00+01:00</published><updated>2026-06-17T00:00:00+01:00</updated><id>https://metriccaution.github.io//2026/06/17/rules-cheatsheet</id><content type="html" xml:base="https://metriccaution.github.io//2026/06/17/rules-cheatsheet/"><![CDATA[<h1 id="rules-cheat-sheets">Rules Cheat-Sheets</h1>

<p>A casual inspection of my Github will make it look like I do little else except play D&amp;D, while I do play most weeks, it’s over-represented in my coding because it’s a domain I understand fairly well, and one with a lot of space for useful helpers - today’s blog is about an RPG tool I’ve built recently.</p>

<p>I’m a long way from the first to write a rules cheat sheet for games I play, but I do think that <a href="https://metriccaution.github.io/rules-cheatsheets/">my version is a useful spin on the idea</a> - I’ve written up a bunch of snippets that either cover a tricky mechanic that I know will be a problem, or answer a specific question during play, each of which is tied back to where to look up the rule itself in the original source.</p>

<p>This started as a React SPA in my <a href="https://metriccaution.github.io/web-snippets/">mono-repo of mini-projects,</a> but I decided to take the time to break it out into its own thing when I wanted to add a couple of other game systems to it, and I’ve used it as an excuse to get a bit more comfortable with Github Actions.</p>

<h2 id="design">Design</h2>

<p>The main design principle here is that I want writing rules snippets to be super simple, and I don’t want to have to think about the framework itself again, unless I need to crack it open to make a site-wide change. The content itself is a few directories of markdown files, with a frontmatter schema, which then get rolled into a basic Metalsmith site generator, by getting converted into structured data, and then fed into the templates.</p>

<p>At that point, writing up a <a href="https://github.com/metriccaution/rules-cheatsheets/blob/main/.github/workflows/deploy.yml">Github action</a> to deploy any changes to <code class="language-plaintext highlighter-rouge">main</code> was pretty simple, so now pretty much any remaining effort with the tool should simply be content authoring.</p>

<h2 id="ai-development">AI Development</h2>

<p>This was one of the first projects I used <a href="https://code.claude.com/docs/en/overview">Claude Code</a> on, and it was both interesting, and a decent lesson in some things it’s very good at, and some things it’s bad at.</p>

<h3 id="problems">Problems</h3>

<p>To get the negative side out of the way first:</p>

<ul>
  <li>I had written up a little framework to get Claude code to validate the info with, if I gave it access to rulebook PDFs, this was pretty hit or miss, honestly, it did catch a couple of issues with games I’m not so familiar with, <em>but</em> it also got quite a lot wrong, persistently so too.</li>
  <li>Getting it to fact-check the ~80 rule hints that I’d already written up ended up chewing through my token limit for the day, so it was pretty expensive honestly!</li>
</ul>

<h3 id="highlights">Highlights</h3>

<p>On the plus side, it genuinely did take care of some things I didn’t really want to spend my spare time doing:</p>

<ul>
  <li>While there’s not much code present to create the site, I had it mostly written before I started using Claude Code, and it understood the structure and flow of data without any prompting.</li>
  <li>Claude’s UI design skills are much better than mine, and while the resulting site is hardly a work of art, I’d been dreading turning my unstyled code into something I could stand to look at, and it just took care of that.</li>
  <li>While it wasn’t great at doing the extra fact checking, it <em>was</em> pretty handy at adding nice markdown styling to my existing rules backlog, and editing for consistency of tone, even if I did spend quite a while fighting it on this too, when it broke the actual rules clarification more than once!</li>
</ul>

<h2 id="looking-forwards">Looking Forwards</h2>

<p>As far as I’m concerned, the code here is pretty much done, but I do intend on keeping on churning out new rules hints for the foreseeable future. As an exercise in Github Actions, this ended up being pretty insubstantial, so I’ll keep an eye out for more things to do there. Finally, I am, unsurprisingly, going to continue to experimenting with Claude.</p>]]></content><author><name>David McGrath</name></author><summary type="html"><![CDATA[Rules Cheat-Sheets]]></summary></entry><entry><title type="html">Boondoggle - Painting By Numbers</title><link href="https://metriccaution.github.io//2025/11/07/boondoggle/" rel="alternate" type="text/html" title="Boondoggle - Painting By Numbers" /><published>2025-11-07T00:00:00+00:00</published><updated>2025-11-07T00:00:00+00:00</updated><id>https://metriccaution.github.io//2025/11/07/boondoggle</id><content type="html" xml:base="https://metriccaution.github.io//2025/11/07/boondoggle/"><![CDATA[<h1 id="boondoggle">Boondoggle</h1>

<p><a href="https://github.com/metriccaution/boondoggle">Repo Page</a>, and a <a href="https://github.com/metriccaution/boondoggle/raw/refs/heads/master/demo-sheet.xlsx">sample output.</a></p>

<p>This is a silly little project that lets you turn images into Excel spreadsheets (though it definitely also works for Google Sheets, and Libre Office), by painting in the backgrounds of cells.</p>

<p><img src="/assets/images/boondoggle.png" alt="A screenshot of the Mona Lisa in Google Sheets" /></p>

<p>It started out as a learning project for <a href="https://poi.apache.org/">Apache POI</a> (a Java library for handling Microsoft office documents) to get up to speed for a project I was doing for work back in 2018. The fact that POI was <a href="https://en.wikipedia.org/wiki/Apache_POI">originally an initialism for “Poor Obfuscation Implementation”</a> is a wonderful bit of trivia, and from what I understand of the internals of XLSX documents, entirely justified.</p>

<p>The idea itself came together pretty quickly, as POI is pretty straightforwards, for what it does. The project ended up being a bit of a crash-course in image compression - as each cell-style is its own entity in one of the XML files that makes up the XLSX file, the number of colours in an image has a drastic affect on conversion speed, which wound up with some <a href="https://en.wikipedia.org/wiki/Color_quantization">color quantization</a> code that <a href="https://github.com/metriccaution/boondoggle/blob/58c38762ba8d021cd4cb49e52ac2c23d3b66849b/bg-compression/src/main/java/com/github/metriccaution/boondoggle/compression/colours/ColourSpaceRestriction.java">ended up being quite educational.</a></p>

<p>After looking at it with fresh eyes years on, I decided to see how straight-forwards I could make it, using some more standard Python libraries (mainly <code class="language-plaintext highlighter-rouge">xlsxwriter</code> and <code class="language-plaintext highlighter-rouge">pillow</code>), and while I do have a certain fondness for the old <a href="https://github.com/metriccaution/boondoggle/tree/58c38762ba8d021cd4cb49e52ac2c23d3b66849b">Java version</a>, I’m impressed with how much cleaner (and more performant around image processing) it is…though that might be the benefit of half a decade of coding speaking. I do find that re-implementations benefit more from understanding the problem space much more than they do from “doing it right” the second time.</p>

<p>Overall, this was a fun little project, and really fits into one of my favourite niches of “really weird software”. If you want to check it out yourself, instructions are in the repo.</p>]]></content><author><name>David McGrath</name></author><summary type="html"><![CDATA[Boondoggle]]></summary></entry><entry><title type="html">A Problem Squared Episode 103 - Caesar Cypher Cycles</title><link href="https://metriccaution.github.io//2025/02/18/problem-squared-caeser-cypher/" rel="alternate" type="text/html" title="A Problem Squared Episode 103 - Caesar Cypher Cycles" /><published>2025-02-18T00:00:00+00:00</published><updated>2025-02-18T00:00:00+00:00</updated><id>https://metriccaution.github.io//2025/02/18/problem-squared-caeser-cypher</id><content type="html" xml:base="https://metriccaution.github.io//2025/02/18/problem-squared-caeser-cypher/"><![CDATA[<h1 id="a-problem-squared-episode-103---caesar-cypher-cycles">A Problem Squared Episode 103 - Caesar Cypher Cycles</h1>

<p><a href="https://podcasters.spotify.com/pod/show/a-problem-squared/episodes/103--Heaps-Splad-and-Pauls-Pad-e2uv2ag">Context</a>.</p>

<p>On the episode, a listener poses the problem:</p>

<blockquote>
  <p>How many English words are also words after they’ve been put through a caesar cypher?</p>

  <p>Is there a pair of words which are caesar cyphers of each other, where there’s a semantic link between the words? Or common theme to them, or some other interesting relationship?</p>
</blockquote>

<p>I thought the episode was great (especially where there’s terrible Python code, so relatable ❤️) - though to my ear, the bit about words cyphering into each other sounded more like finding cycles (so something where <code class="language-plaintext highlighter-rouge">Word A</code> -&gt; <code class="language-plaintext highlighter-rouge">Word B</code> -&gt; <code class="language-plaintext highlighter-rouge">Word C</code> -&gt; <code class="language-plaintext highlighter-rouge">Word A</code>), so I had a go at answering that.</p>

<h2 id="approach">Approach</h2>

<p>While I was listening along, I was thinking about how I’d code it up, and came up with a way to calculate this fairly simply. I.e. words can be grouped together by how each letter of the word changes from the preceding one.</p>

<p>It’s probably easier to give an example, from the episode <a href="https://gchq.github.io/CyberChef/#recipe=ROT13(true,true,false,4)&amp;input=YmVj"><code class="language-plaintext highlighter-rouge">ROT4(bec) = fig</code></a>.</p>

<blockquote>
  <p>The code below handles this a little differently, but explaining it this way round is a little simpler.</p>
</blockquote>

<ul>
  <li><code class="language-plaintext highlighter-rouge">b</code> to <code class="language-plaintext highlighter-rouge">e</code> is +3</li>
  <li><code class="language-plaintext highlighter-rouge">e</code> to <code class="language-plaintext highlighter-rouge">c</code> is +24 (looping around <code class="language-plaintext highlighter-rouge">z</code> to <code class="language-plaintext highlighter-rouge">a</code>)</li>
</ul>

<p>So the “signature” vector for <code class="language-plaintext highlighter-rouge">bec</code> is <code class="language-plaintext highlighter-rouge">[3, 24]</code>.</p>

<p>Similarly:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">f</code> to <code class="language-plaintext highlighter-rouge">i</code> is 3</li>
  <li><code class="language-plaintext highlighter-rouge">i</code> to <code class="language-plaintext highlighter-rouge">g</code> is 24</li>
</ul>

<p>So <code class="language-plaintext highlighter-rouge">fig</code> is also <code class="language-plaintext highlighter-rouge">[3, 24]</code>.</p>

<p>However, there’s no way to turn cypher <code class="language-plaintext highlighter-rouge">cat</code> into <code class="language-plaintext highlighter-rouge">bec</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">c</code> to <code class="language-plaintext highlighter-rouge">a</code> is 24</li>
  <li><code class="language-plaintext highlighter-rouge">a</code> to <code class="language-plaintext highlighter-rouge">t</code> is 19</li>
</ul>

<p>So <code class="language-plaintext highlighter-rouge">cat</code> is <code class="language-plaintext highlighter-rouge">[24, 19]</code>.</p>

<p>I haven’t proved it rigorously, but it makes intuitive sense if you think about building the word letter by letter, the two inputs to making a word are the starting letter, and then how to move from letter to letter, until the end of the word.</p>

<p>This approach means that we can group all the words up into groups that can be cyphered into each other with a single pass through the dictionary, by working out this vector for every word.</p>

<p>Then, once we’ve grouped up, we can examine the contents of each group for cycles.</p>

<h2 id="answers">Answers</h2>

<p>As a caveat, the words list I’m using is whatever the default on Ubuntu is (<code class="language-plaintext highlighter-rouge">/usr/share/dict/words</code>), I’m not quite sure where it’s actually from originally - it seems pretty good, but your answers may vary, and I’m certainly missing some of the words in Matt’s dictionary.</p>

<p>I’ve also excluded any words shorter than 3 letters, as they don’t seem to be that interesting.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /usr/share/dict/words | python3 caesar.py
</code></pre></div></div>

<ul>
  <li><em>How many English words are also words after they’ve been put through a caesar cypher?</em>
    <ul>
      <li>1410 words</li>
    </ul>
  </li>
  <li><em>Is there a pair of words which are caesar cyphers of each other</em>
    <ul>
      <li>I’ve found 56 pairs of words that cycle from one to the other, and back using <code class="language-plaintext highlighter-rouge">ROT13</code>
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ROT13</code> is significant here as doing it twice takes you back to the original input</li>
        </ul>
      </li>
      <li>My favourites here are <em>green terra</em>, and Typhoid Mary’s lesser known rival, <em>Ebola Robyn</em></li>
      <li>I don’t think there are any cycles longer than 2 words, as the prime factors of the number of letters in the alphabet (<code class="language-plaintext highlighter-rouge">26</code>) are <code class="language-plaintext highlighter-rouge">2</code>, and <code class="language-plaintext highlighter-rouge">13</code>
        <ul>
          <li>The only cycle lengths possible are divisors of <code class="language-plaintext highlighter-rouge">26</code>, if you want to get back to where you started (so <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">2</code>, <code class="language-plaintext highlighter-rouge">13</code>, <code class="language-plaintext highlighter-rouge">26</code>).</li>
          <li>This means the next shortest cycle will be 13 items long, and the largest actual group of words with the same signature is 6 words (<code class="language-plaintext highlighter-rouge">aol,bpm,esp,max,rfc,the</code> being the closest to not gibberish here)</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="the-code">The Code</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">csv</span>
<span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Generator</span><span class="p">,</span> <span class="n">List</span>

<span class="c1"># Vector generation
</span>

<span class="k">def</span> <span class="nf">distance</span><span class="p">(</span><span class="n">c1</span><span class="p">:</span> <span class="nb">chr</span><span class="p">,</span> <span class="n">c2</span><span class="p">:</span> <span class="nb">chr</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"Rotation distance from here to there."</span>

    <span class="k">return</span> <span class="p">(</span><span class="nb">ord</span><span class="p">(</span><span class="n">c1</span><span class="p">)</span> <span class="o">-</span> <span class="nb">ord</span><span class="p">(</span><span class="n">c2</span><span class="p">))</span> <span class="o">%</span> <span class="mi">26</span>


<span class="k">def</span> <span class="nf">diffs</span><span class="p">(</span><span class="n">word</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"Work out a key to group words on for cypher-equality."</span>

    <span class="n">char_diffs</span> <span class="o">=</span> <span class="p">[</span><span class="n">distance</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">word</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">word</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">lower</span><span class="p">())]</span>
    <span class="k">return</span> <span class="s">";"</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">char_diffs</span><span class="p">)</span>


<span class="c1"># Text processing
</span>

<span class="k">def</span> <span class="nf">clean_word</span><span class="p">(</span><span class="n">word</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"Tidy up a word so we don't get messed up on capital letters etc."</span>
    <span class="k">return</span> <span class="n">word</span><span class="p">.</span><span class="n">strip</span><span class="p">().</span><span class="n">lower</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">words_from_stdin</span><span class="p">(</span><span class="n">min_word_length</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Generator</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">]:</span>
    <span class="s">"Read data piped in, normalise it, and filter down to actual words."</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="nb">map</span><span class="p">(</span><span class="n">clean_word</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">stdin</span><span class="p">):</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">word</span><span class="p">.</span><span class="n">isalpha</span><span class="p">():</span>
            <span class="k">continue</span>

        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">min_word_length</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="k">yield</span> <span class="n">word</span>


<span class="c1"># Answering problems
</span>

<span class="k">def</span> <span class="nf">find_cycles</span><span class="p">(</span><span class="n">word_list</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">Generator</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">]:</span>
    <span class="s">"Spit out any cycles in a word list."</span>

    <span class="c1"># While this isn't generally the case, realistically, we're only looking for ROT-13.
</span>    <span class="c1"># Otherwise, the next shortest cycle will be 13 items long, and it gets worse from there.
</span>    <span class="c1"># Once we ignore words 2 characters or less, the biggest group is 6 items.
</span>    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">word</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">word_list</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">other_word</span> <span class="ow">in</span> <span class="n">word_list</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">:]:</span>
            <span class="k">if</span> <span class="n">word</span> <span class="o">==</span> <span class="n">other_word</span><span class="p">:</span>
                <span class="k">continue</span>

            <span class="k">if</span> <span class="n">distance</span><span class="p">(</span><span class="n">word</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">other_word</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">==</span> <span class="mi">13</span><span class="p">:</span>
                <span class="k">yield</span> <span class="p">[</span><span class="n">word</span><span class="p">,</span> <span class="n">other_word</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">answer_question</span><span class="p">(</span><span class="n">word_list</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">report_file</span><span class="p">:</span> <span class="n">Path</span><span class="p">):</span>
    <span class="s">"Actually answer the question."</span>

    <span class="n">unique_words</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">words</span> <span class="ow">in</span> <span class="n">word_list</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">words</span><span class="p">:</span>
            <span class="n">unique_words</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>

    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">report_file</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">w</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
        <span class="n">w</span><span class="p">.</span><span class="n">writerow</span><span class="p">([</span><span class="s">"How many words?"</span><span class="p">])</span>
        <span class="n">w</span><span class="p">.</span><span class="n">writerow</span><span class="p">([</span><span class="nb">len</span><span class="p">(</span><span class="n">unique_words</span><span class="p">)])</span>


<span class="k">def</span> <span class="nf">output_cycles</span><span class="p">(</span><span class="n">word_list</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">report_file</span><span class="p">:</span> <span class="n">Path</span><span class="p">):</span>
    <span class="s">"Write out lists of words where cyphering loops."</span>

    <span class="c1"># Sort by word length
</span>    <span class="n">grouped</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">word_list</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>

    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">report_file</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">w</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">g</span> <span class="ow">in</span> <span class="n">grouped</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">w1</span><span class="p">,</span> <span class="n">w2</span> <span class="ow">in</span> <span class="n">find_cycles</span><span class="p">(</span><span class="n">g</span><span class="p">):</span>
                <span class="n">w</span><span class="p">.</span><span class="n">writerow</span><span class="p">([</span><span class="n">w1</span><span class="p">,</span> <span class="n">w2</span><span class="p">])</span>


<span class="k">def</span> <span class="nf">write_all</span><span class="p">(</span><span class="n">word_list</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">report_file</span><span class="p">:</span> <span class="n">Path</span><span class="p">):</span>
    <span class="s">"Write out all of the groups of cypherable words"</span>

    <span class="c1"># Sort by group length
</span>    <span class="n">grouped</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">word_list</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="o">-</span><span class="mi">1000</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">+</span> <span class="o">-</span><span class="mi">1</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>

    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">report_file</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">w</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">g</span> <span class="ow">in</span> <span class="n">grouped</span><span class="p">:</span>
            <span class="n">w</span><span class="p">.</span><span class="n">writerow</span><span class="p">(</span><span class="n">g</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Starting"</span><span class="p">)</span>

    <span class="n">grouped</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">word</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">words_from_stdin</span><span class="p">(</span><span class="n">min_word_length</span><span class="o">=</span><span class="mi">3</span><span class="p">)):</span>
        <span class="n">cypher_id</span> <span class="o">=</span> <span class="n">diffs</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">cypher_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">grouped</span><span class="p">:</span>
            <span class="n">grouped</span><span class="p">[</span><span class="n">cypher_id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>

        <span class="n">grouped</span><span class="p">[</span><span class="n">cypher_id</span><span class="p">].</span><span class="n">add</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>

    <span class="n">grouped</span> <span class="o">=</span> <span class="p">[</span><span class="nb">sorted</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">grouped</span><span class="p">.</span><span class="n">values</span><span class="p">()</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">]</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Checked </span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s"> words, and found </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">grouped</span><span class="p">)</span><span class="si">}</span><span class="s"> clusters of words"</span><span class="p">)</span>

    <span class="n">report_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="p">()</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Writing results out to </span><span class="si">{</span><span class="n">report_dir</span><span class="p">.</span><span class="n">absolute</span><span class="p">()</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

    <span class="n">answer_question</span><span class="p">(</span><span class="n">grouped</span><span class="p">,</span> <span class="n">Path</span><span class="p">(</span><span class="n">report_dir</span><span class="p">,</span> <span class="s">"answers.csv"</span><span class="p">))</span>
    <span class="n">write_all</span><span class="p">(</span><span class="n">grouped</span><span class="p">,</span> <span class="n">Path</span><span class="p">(</span><span class="n">report_dir</span><span class="p">,</span> <span class="s">"cypher-groups.csv"</span><span class="p">))</span>
    <span class="n">output_cycles</span><span class="p">(</span><span class="n">grouped</span><span class="p">,</span> <span class="n">Path</span><span class="p">(</span><span class="n">report_dir</span><span class="p">,</span> <span class="s">"cycles.csv"</span><span class="p">))</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name>David McGrath</name></author><summary type="html"><![CDATA[A Problem Squared Episode 103 - Caesar Cypher Cycles]]></summary></entry><entry><title type="html">Text to Speech for Content Consumption</title><link href="https://metriccaution.github.io//2024/03/04/tts-content-consumption/" rel="alternate" type="text/html" title="Text to Speech for Content Consumption" /><published>2024-03-04T00:00:00+00:00</published><updated>2024-03-04T00:00:00+00:00</updated><id>https://metriccaution.github.io//2024/03/04/tts-content-consumption</id><content type="html" xml:base="https://metriccaution.github.io//2024/03/04/tts-content-consumption/"><![CDATA[<h1 id="text-to-speech-for-content-consumption">Text to Speech for Content Consumption</h1>

<p>Over the years, I’ve built up quite a lot of content that I’ve meant to read “some day”, and I’ve also been trying to cut down on my screen time. A discussion <a href="https://news.ycombinator.com/item?id=39442882">on HackerNews</a> prompted me to try out solving these two problems using text-to-speech (TTS) to listen to my reading list.</p>

<p>I’ve had moderate success with this (at least for some types of content - diagram or code-heavy content is not enhanced by TTS).</p>

<h1 id="technology">Technology</h1>

<p>I generally do most of my scripting in Python or Deno, so I wanted to have something available using one of these.</p>

<p>Some exploration around the ecosystem makes <a href="https://github.com/rhasspy/piper/tree/master">Piper</a> look like a good TTS package to use, as it’s fairly straight-forwards to use, has multiple voices available, and actually sounds fairly good.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">piper</span> <span class="kn">import</span> <span class="n">PiperVoice</span>
<span class="kn">import</span> <span class="nn">wave</span>

<span class="n">voice</span> <span class="o">=</span> <span class="n">PiperVoice</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">model_path</span><span class="o">=</span><span class="s">"./models/en_GB-alan-medium.onnx"</span><span class="p">,</span> <span class="n">use_cuda</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"./output.wav"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"wb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
  <span class="k">with</span> <span class="n">wave</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"wb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">wav_file</span><span class="p">:</span>
    <span class="n">voice</span><span class="p">.</span><span class="n">synthesize</span><span class="p">(</span><span class="s">"Hello world"</span><span class="p">,</span> <span class="n">wav_file</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="pre-processing">Pre-processing</h2>

<p>I’ve ended up having to have some pre-processing scripts for some of the content I’ve been consuming with this - mostly around removing things like footnote links, or making numbered lists read nicely.</p>

<p>At present, this is a cluster of very bespoke Python functions.</p>

<h2 id="post-processing">Post-processing</h2>

<p>The <code class="language-plaintext highlighter-rouge">.wav</code> files output by Piper are (as-expected), pretty huge, but a very simple <a href="https://ffmpeg.org/">FFMpeg</a> script solves this by converting over to <code class="language-plaintext highlighter-rouge">mp3</code>s:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> ./output.wav ./output.mp3
</code></pre></div></div>

<p>It’s probably possible to do something a little more refined, but this got me far enough!</p>

<h2 id="other-scaffolding">Other Scaffolding</h2>

<p>I’ve put together a small script locally that means I can keep a directory of text files, and only run TTS on new ones, as well as some scripting around getting the resulting files onto my phone, in a <a href="https://en.wikipedia.org/wiki/XML_Shareable_Playlist_Format">XSPF</a> playlist for VLC.</p>

<h1 id="follow-up-work">Follow-Up Work</h1>

<p>Some things I intend to do here, but haven’t yet:</p>

<ol>
  <li>Scripting fetching the text from websites, and running TTS on them (again, inspired by the HN post)</li>
  <li>Some tooling around using TTS for proof-reading, as I find it much easier to spot typos and awkward phrasing when listening</li>
  <li>Finding a slightly less bespoke way of cleaning up the text sources I’m feeding into TTS</li>
  <li>Open sourcing the project if I can make a compelling end-to-end solution</li>
</ol>]]></content><author><name>David McGrath</name></author><summary type="html"><![CDATA[Text to Speech for Content Consumption]]></summary></entry></feed>