<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>taf.codes</title>
 <link href="https://taf.codes/atom.xml" rel="self"/>
 <link href="https://taf.codes/"/>
 <updated>2026-05-14T19:49:46-04:00</updated>
 <id>https://taf.codes</id>
 <author>
   <name></name>
   <email></email>
 </author>

 
 <entry>
   <title>Podcast Trend Analysis -- Part 1</title>
   <link href="https://taf.codes/2026/05/03/podcasts-2-trends.html"/>
   <updated>2026-05-03T00:00:00-04:00</updated>
   <id>https://taf.codes/2026/05/03/podcasts-2-trends</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;  I can start doing real analysis of trends in semantic-space, but slowly.  It turns out that the queries I’m interested in are not necessarily suited to ANN-based approximations.&lt;/p&gt;

&lt;h3 id=&quot;what&quot;&gt;What?&lt;/h3&gt;
&lt;p&gt;By now, I’ve started to amass a decently large index of podcast content, enough to compute interesting statistics.  &lt;a href=&quot;#appendix-trend-examples&quot;&gt;Here are several examples of trends that my system is sensitive to.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;#trends-queries&quot;&gt;current implementation&lt;/a&gt; is naive: for a given query, the database scans every chunk of content and computes a relevance score for each, then these go into the statistics.  This is slow, but it’s easy to reason about and performs predictably.  A significantly faster way to do something similar would make use of the HNSW index to get the top-k approximate nearest neighbors to the query vector, then compute statistics exclusively from those k most relevant chunks, ignoring the “irrelevant” chunks entirely.&lt;/p&gt;

&lt;p&gt;Indeed, that had been my plan from the beginning.  With the exact queries working as expected, I could create their approximate equivalents and compare them to the exact variants empirically.  However, I’ve been thinking about the degree and nature of information-loss that occurs as an inevitability of that approximation, and I’m starting to think the exact scanning approach may be more suitable.&lt;/p&gt;

&lt;h4 id=&quot;the-case-for-exactness&quot;&gt;The Case for Exactness&lt;/h4&gt;

&lt;p&gt;Using the HNSW index to run an approximate search for the top-k would almost certainly return a perfectly adequate top-k compared to the ground truth of running an exact scan and sorting by distance.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;   For reasonable values of k, it will also be much faster.&lt;/p&gt;

&lt;p&gt;What exactly is “a reasonable value of k”, though?  In terms of speed (latency and throughput), a rule of thumb I see repeated is that when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k &amp;lt; ~1% of N&lt;/code&gt; HNSW should reliably outperform an exact scan, and that by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k &amp;gt; ~10% of N&lt;/code&gt; an exact scan starts to look preferable. Intuitively, this makes perfect sense to me simply by virtue of the sequential access pattern for an exact scan.  Even in memory, random access (graph traversal, in this case) might easily reduce throughput by a factor of 10.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;This consideration of latency/throughput poses a question: essentially “what percentage of my data am I willing to ignore?”.  If I can ignore ~99% of my data, k can be ~1% of N, and HNSW is a great deal.  If I ignore merely ~90% of my data, k would be ~10% of N, and HNSW is no faster than an exact scan.  Meanwhile, the exact scan can capture signal from 100% of my data.&lt;/p&gt;

&lt;p&gt;In the case of a search for specific information, it’s very reasonable to ignore 99% of the data.  In a trend analysis context, it’s often less reasonable.  That’s not to say it’s anywhere near &lt;em&gt;un&lt;/em&gt;-reasonable!  In many real-world cases, the majority of the signal is in 1% of the data, and in some of those cases it’ll be the &lt;em&gt;overwhelming&lt;/em&gt; majority of the signal.  However, not all signals are so concentrated.  This relates to the “broadness” or the “narrowness” of the topic, and how you want to treat relevance.&lt;/p&gt;

&lt;p&gt;A “narrow” query is similar to tallying all the high-relevance search results of a specific area of concern, such as “new world-record for largest cabbage”.  Chunks of content about cabbages generally, or even about unusually large cabbages, are irrelevant; we only want to find out when a new record has been set.&lt;/p&gt;

&lt;p&gt;A “broad” query is something more like “deep-sea mining”, where we might even want to give some degree of fractional relevance to results about deep-sea minerals (even if mining isn’t mentioned) or to results about mining generally (even if it’s not strictly about the deep-sea).  As long as we’re able to give correspondingly increased weight to results with correspondingly more relevance, there’s potential utility in such a signal.&lt;/p&gt;

&lt;p&gt;For a closer look at how I’m handling fractional relevance, check out &lt;a href=&quot;#spatial-decay&quot;&gt;this section&lt;/a&gt;.  There’s also a &lt;a href=&quot;#filtering&quot;&gt;tangent about how HNSW interacts with filters, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE&lt;/code&gt; clauses in SQL,&lt;/a&gt; which was relevant in my decision to lean into exact scan processing.&lt;/p&gt;

&lt;p&gt;In short, I think these “broad” queries are interesting, and I would like to be able to squeeze as much information out of them as possible.  Doing this puts me into the realm where HNSW doesn’t have much advantage over an exact scan, &lt;em&gt;and&lt;/em&gt; it results in non-zero information loss.  At that point, an exact flat scan starts to look pretty good.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h3&gt;

&lt;p&gt;Making flat scans faster!  Step one is more memory, step two is moving to something columnar (probably DuckDB).  Eventually, I may want to run some experiments to handle NUMA better.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;appendix-trend-examples&quot;&gt;Appendix: Trend Examples&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The RAMpocalypse&lt;/strong&gt;
&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;../../../assets/img/podcasts-2/ram-crisis-time.png&quot; alt=&quot;A timeline of the rate of relevance to the query &apos;computer hardware is getting expensive&apos; showing a notable spike around January 2026.&quot; style=&quot;margin-left:2.5em; width:80%; border: 0.25rem solid; border-color: #3f3c8a;&quot; /&gt; &lt;br /&gt;
This spike in podcast coverage of “computer hardware getting expensive” lines up with the &lt;a href=&quot;https://pcpartpicker.com/trends/price/memory/&quot;&gt;surge in memory prices&lt;/a&gt;.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which feeds are “scary true crime”?&lt;/strong&gt;
&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;../../../assets/img/podcasts-2/scary-true-crime-by-feed.png&quot; alt=&quot;A 2-column view showing obviously true-crime related feed titles as having highly above-median relevance rates to the query &apos;scary true crime&apos;.  The number one is called &apos;Morning Cup of Murder&apos;.&quot; style=&quot;margin-left:2.5em; width:80%; border: 0.25rem solid; border-color: #3f3c8a;&quot; /&gt; &lt;br /&gt;
This view compares the relevance-rate of chunks on a by-feed basis.  The left column shows the feeds that are most above the median rate, and the right column shows the feeds that are the most below the median rate.  If you’re curious about those “sigmoid parameters”, check out the &lt;a href=&quot;#spatial-decay&quot;&gt;explanation&lt;/a&gt;.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Models from China?&lt;/strong&gt;
&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;../../../assets/img/podcasts-2/deepseek-by-time.png&quot; alt=&quot;A timeline of the rate of relevance to the query &apos;chinese artificial intelligence model competing with united states artificial intelligence companies&apos; showing a notable spike around January 2025.&quot; style=&quot;margin-left:2.5em; width:80%; border: 0.25rem solid; border-color: #3f3c8a;&quot; /&gt; &lt;br /&gt;
Do you remember when everyone was talking about DeepSeek?  I myself didn’t remember precisely when, but the data clearly indicates it was January 2025.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tariffs&lt;/strong&gt;
&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;../../../assets/img/podcasts-2/tariffs-by-time.png&quot; alt=&quot;A timeline of the rate of relevance to the query &apos;tariffs are impacting the economy but nobody is quite sure how&apos; showing a notable spike in April 2025, with a broader elevation from February 2025 to June 2025.&quot; style=&quot;margin-left:2.5em; width:80%; border: 0.25rem solid; border-color: #3f3c8a;&quot; /&gt; &lt;br /&gt;
This chart shows something that I recall: podcasts talked about tariffs for a longer period of time than they talked about &lt;a href=&quot;#ai-models-from-china&quot;&gt;DeepSeek&lt;/a&gt;.  Of course the main spike occurs around &lt;a href=&quot;https://en.wikipedia.org/wiki/Liberation_Day_tariffs&quot;&gt;April 2&lt;/a&gt;, but we can see a lasting elevation.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Financial News Coverage&lt;/strong&gt;
&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;../../../assets/img/podcasts-2/financial-coverage-by-feed.png&quot; alt=&quot;A view of the feeds most and least pertaining to financial news coverage.&quot; style=&quot;margin-left:2.5em; width:80%; border: 0.25rem solid; border-color: #3f3c8a;&quot; /&gt; &lt;br /&gt;
This EZ News show seems to &lt;a href=&quot;https://podcasts.apple.com/tw/podcast/ez-news/id1435995201&quot;&gt;open every episode with a stock quote&lt;/a&gt;.  Predictably, the Stoics don’t talk much about stocks.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;build-notes&quot;&gt;Build Notes&lt;/h3&gt;

&lt;h4 id=&quot;trends-queries&quot;&gt;Trends Queries&lt;/h4&gt;

&lt;p&gt;You can check out the &lt;a href=&quot;https://forge.taf.codes/taf/podcast_search/src/commit/b2c8a46132443fe2ddd3454678f82896cf9c6e79/podsearch/lib/podsearch/trends.ex&quot;&gt;source&lt;/a&gt;, but there’s not too much to know performance-wise: queries are flat, exact scans.  The table is currently around 16GB.  When it’s sitting on the disk and not cached (such as on a cheap VPS without that much RAM), the absolute upper bound for performance is simply how fast you can read 16GB off the disk.  Even if this was a perfectly sequential access pattern (which it isn’t), on a fast disk, the query would take multiple seconds.&lt;/p&gt;

&lt;p&gt;Without thinking deeply about Postgres internals, I know that, at a minimum, the vectors I need are &lt;a href=&quot;https://www.postgresql.org/docs/9.5/storage-toast.html&quot;&gt;TOAST&lt;/a&gt;‘d, so there’s at least one layer of indirection between them and being sequentially flat scanned.  In reality, I can be certain just by the readily observable runtime performance characteristics and background knowledge that there’s more indirection than that!&lt;/p&gt;

&lt;p&gt;There’s more than one way I could make the scan faster in Postgres.  The only one I had been seriously considering was to use ANN to approximate my query.  &lt;a href=&quot;#the-case-for-exactness&quot;&gt;Having decided against that&lt;/a&gt; for now, I think it’s time to admit that I am clearly in OLAP territory, and I should probably stop trying to get good results from an OLTP-leaning database.&lt;/p&gt;

&lt;h4 id=&quot;filtering&quot;&gt;Filtering&lt;/h4&gt;

&lt;p&gt;With a single HNSW index on a single table, you don’t necessarily gain performance from filtering out a lot of rows.  In fact, with pgvector (and presumably other implementations), you lose performance!  Essentially, the ANN step of the query has to be first, because the HNSW graph is not aware of anything but the vectors themselves.  Filters are applied afterward, to the k results returned.&lt;/p&gt;

&lt;p&gt;For a concrete example, I have a table with content-chunks stretching back over 18 months.  If I want to compute statistics over the last 1 month, I &lt;em&gt;don’t&lt;/em&gt; get to chop the table down to 1/18th of its size and then run ANN on that small piece.&lt;/p&gt;

&lt;p&gt;If you want the 1,000 approximate-nearest results from the entire date range, you can simply query with k=1,000 and get the result that you want.  If instead you want the 1,000 approximate-nearest results &lt;em&gt;within 1 given month out of the 18&lt;/em&gt; you’d need to use much larger k.  In this scenario, you could try k=18,000 on the assumption that the results are evenly distributed across time.  However, &lt;a href=&quot;#appendix-trend-examples&quot;&gt;even a cursory glance will show this to be a visibly poor assumption.&lt;/a&gt;&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;   So, even with a filter which isn’t very selective, you might find &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; growing out of control very quickly, depending on how the data is distributed.&lt;/p&gt;

&lt;p&gt;This of course interacts very poorly with &lt;a href=&quot;#the-case-for-exactness&quot;&gt;everything I wrote about earlier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pgvector does have iterative index scans, but that’s not the same thing as filter-aware traversal.  If I understand correctly, it’s just to keep you from having to guess the right &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; ahead of time: dynamically extending the traversal until enough candidates survive filtering.  It looks like &lt;a href=&quot;https://github.com/qdrant/qdrant&quot;&gt;Qdrant&lt;/a&gt; and &lt;a href=&quot;https://github.com/weaviate/weaviate&quot;&gt;Weaviate&lt;/a&gt; are doing something more like “filter-aware traversal”.&lt;/p&gt;

&lt;p&gt;At a glance, it looks like they are likely outperforming pgvector in the sense that you can apply filters that are more selective before you fall into the territory where a scan beats ANN.  My impression is that they’re buying more ground for the general case of filtering, but, if I wanted to run the fastest approximate queries in the very specific case I described above, I might be better served by simply giving each month of data its own HNSW index.&lt;/p&gt;

&lt;h4 id=&quot;spatial-decay&quot;&gt;Spatial Decay&lt;/h4&gt;

&lt;p&gt;Before I had thought about it much, I figured it would be interesting to show trends in terms of how many relevant results existed in a given feed, or in a given time period to create something like the charts above.  I quickly had to confront the question: what is “relevant”, exactly?&lt;/p&gt;

&lt;p&gt;One might use a threshold.  With the embedding model that I’m using, the vibes are roughly so:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Similarity Score&lt;/th&gt;
      &lt;th&gt;Search Result Quality&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;0.45 or lower&lt;/td&gt;
      &lt;td&gt;not likely good&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0.5&lt;/td&gt;
      &lt;td&gt;might be good&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0.55&lt;/td&gt;
      &lt;td&gt;likely good&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0.6 or higher&lt;/td&gt;
      &lt;td&gt;very likely good&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Why not use 0.55 as a threshold and count the results?  This is not unreasonable, but it is discarding information that might be useful.  It’s collecting only 1 bit of information per chunk of content.  For your consideration, here’s what a photographic image looks like when each pixel carries only one bit of information:&lt;/p&gt;

&lt;!-- Image bit-depth / tone-mapping toy. Entirely client-side. --&gt;
&lt;!-- Save as _includes/bitdepth-toy.html; the JS lives in assets/js/. --&gt;
&lt;div class=&quot;bdt-root&quot; data-default-image=&quot;/assets/img/podcasts-2/peony.jpg&quot;&gt;
  &lt;div class=&quot;bdt-controls&quot;&gt;
    &lt;label class=&quot;bdt-upload-label&quot; for=&quot;bdt-upload&quot;&gt;
      &lt;svg width=&quot;14&quot; height=&quot;14&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4&quot; /&gt;&lt;polyline points=&quot;17 8 12 3 7 8&quot; /&gt;&lt;line x1=&quot;12&quot; y1=&quot;3&quot; x2=&quot;12&quot; y2=&quot;15&quot; /&gt;&lt;/svg&gt;
      Upload image
    &lt;/label&gt;
    &lt;input type=&quot;file&quot; id=&quot;bdt-upload&quot; accept=&quot;image/*&quot; /&gt;
    &lt;button id=&quot;bdt-sample&quot; type=&quot;button&quot;&gt;Use sample&lt;/button&gt;
    &lt;button id=&quot;bdt-invert&quot; type=&quot;button&quot;&gt;Invert&lt;/button&gt;
  &lt;/div&gt;

  &lt;div class=&quot;bdt-canvas-wrap&quot;&gt;
    &lt;canvas id=&quot;bdt-canvas&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;

  &lt;div class=&quot;bdt-row&quot;&gt;
    &lt;label for=&quot;bdt-mode&quot;&gt;Mode&lt;/label&gt;
    &lt;select id=&quot;bdt-mode&quot;&gt;
      &lt;optgroup label=&quot;Quantization&quot;&gt;
        &lt;option value=&quot;1&quot;&gt;1-bit threshold (2 levels)&lt;/option&gt;
        &lt;option value=&quot;2&quot;&gt;2-bit (4 levels)&lt;/option&gt;
        &lt;option value=&quot;4&quot;&gt;4-bit (16 levels)&lt;/option&gt;
      &lt;/optgroup&gt;
      &lt;optgroup label=&quot;Tone mapping&quot;&gt;
        &lt;option value=&quot;sigmoid&quot;&gt;Sigmoid (continuous)&lt;/option&gt;
      &lt;/optgroup&gt;
    &lt;/select&gt;
  &lt;/div&gt;

  &lt;div class=&quot;bdt-row&quot; id=&quot;bdt-threshold-row&quot;&gt;
    &lt;label for=&quot;bdt-threshold&quot;&gt;Threshold&lt;/label&gt;
    &lt;input type=&quot;range&quot; min=&quot;0&quot; max=&quot;255&quot; value=&quot;128&quot; step=&quot;1&quot; id=&quot;bdt-threshold&quot; /&gt;
    &lt;span id=&quot;bdt-threshold-out&quot;&gt;128&lt;/span&gt;
  &lt;/div&gt;

  &lt;div id=&quot;bdt-sigmoid-section&quot;&gt;
    &lt;div id=&quot;bdt-sigmoid-rows&quot;&gt;
      &lt;div class=&quot;bdt-row&quot; id=&quot;bdt-center-row&quot;&gt;
        &lt;label for=&quot;bdt-center&quot;&gt;Center&lt;/label&gt;
        &lt;input type=&quot;range&quot; min=&quot;0&quot; max=&quot;255&quot; value=&quot;128&quot; step=&quot;1&quot; id=&quot;bdt-center&quot; /&gt;
        &lt;span id=&quot;bdt-center-out&quot;&gt;128&lt;/span&gt;
      &lt;/div&gt;

      &lt;div class=&quot;bdt-row&quot; id=&quot;bdt-steepness-row&quot;&gt;
        &lt;label for=&quot;bdt-steepness&quot;&gt;Steepness&lt;/label&gt;
        &lt;input type=&quot;range&quot; min=&quot;1&quot; max=&quot;50&quot; value=&quot;10&quot; step=&quot;1&quot; id=&quot;bdt-steepness&quot; /&gt;
        &lt;span id=&quot;bdt-steepness-out&quot;&gt;10&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div id=&quot;bdt-curve-wrap&quot;&gt;
      &lt;svg id=&quot;bdt-curve&quot; viewBox=&quot;0 0 100 100&quot; preserveAspectRatio=&quot;xMidYMid meet&quot; aria-label=&quot;Sigmoid tone mapping curve&quot;&gt;
        &lt;defs&gt;
          &lt;linearGradient id=&quot;bdt-x-grad&quot; x1=&quot;0&quot; x2=&quot;1&quot; y1=&quot;0&quot; y2=&quot;0&quot;&gt;
            &lt;stop offset=&quot;0&quot; stop-color=&quot;#000&quot; /&gt;
            &lt;stop offset=&quot;1&quot; stop-color=&quot;#fff&quot; /&gt;
          &lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;bdt-y-grad&quot; x1=&quot;0&quot; x2=&quot;0&quot; y1=&quot;0&quot; y2=&quot;1&quot;&gt;
            &lt;stop offset=&quot;0&quot; stop-color=&quot;#fff&quot; /&gt;
            &lt;stop offset=&quot;1&quot; stop-color=&quot;#000&quot; /&gt;
          &lt;/linearGradient&gt;
        &lt;/defs&gt;
        &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;6&quot; height=&quot;94&quot; fill=&quot;url(#bdt-y-grad)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot; stroke-width=&quot;0.4&quot; /&gt;
        &lt;rect x=&quot;6&quot; y=&quot;94&quot; width=&quot;94&quot; height=&quot;6&quot; fill=&quot;url(#bdt-x-grad)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot; stroke-width=&quot;0.4&quot; /&gt;
        &lt;rect x=&quot;6&quot; y=&quot;0&quot; width=&quot;94&quot; height=&quot;94&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot; stroke-width=&quot;0.4&quot; /&gt;
        &lt;line id=&quot;bdt-center-line&quot; y1=&quot;0&quot; y2=&quot;94&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.3&quot; stroke-width=&quot;0.4&quot; stroke-dasharray=&quot;2 2&quot; /&gt;
        &lt;path id=&quot;bdt-curve-path&quot; fill=&quot;none&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; /&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;style&gt;
.bdt-root { font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, sans-serif; font-size: 14px; color: #222; width: 80%; max-width: 700px; margin: 1.5em auto; }
.bdt-root *, .bdt-root *::before, .bdt-root *::after { box-sizing: border-box; }
.bdt-controls { display: flex; gap: 8px; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }
.bdt-upload-label { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1px solid #d0d0d0; border-radius: 8px; cursor: pointer; background: #fff; color: inherit; line-height: 1.2; }
.bdt-upload-label:hover { background: #f5f5f5; }
.bdt-root input[type=file] { display: none; }
.bdt-root button { padding: 8px 14px; border: 1px solid #d0d0d0; border-radius: 8px; background: #fff; cursor: pointer; font-size: 14px; font-family: inherit; color: inherit; line-height: 1.2; }
.bdt-root button:hover { background: #f5f5f5; }
.bdt-canvas-wrap { background: #ececec; border-radius: 12px; padding: 1rem; display: flex; align-items: center; justify-content: center; min-height: 300px; margin-bottom: 1rem; }
.bdt-canvas-wrap canvas { max-width: 100%; height: auto; display: block; image-rendering: pixelated; border-radius: 4px; }
.bdt-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.bdt-row &gt; label { min-width: 76px; color: #666; }
.bdt-row &gt; select, .bdt-row &gt; input[type=range] { flex: 1; font-family: inherit; font-size: 14px; min-width: 0; }
.bdt-row &gt; select { padding: 6px 8px; border: 1px solid #d0d0d0; border-radius: 6px; background: #fff; color: inherit; }
.bdt-row &gt; span { min-width: 32px; text-align: right; font-weight: 500; }
#bdt-sigmoid-section { display: none; gap: 14px; align-items: center; margin-bottom: 12px; }
#bdt-sigmoid-rows { flex: 1; min-width: 0; }
#bdt-sigmoid-rows .bdt-row:last-child { margin-bottom: 0; }
#bdt-curve-wrap { width: 72px; flex-shrink: 0; }
#bdt-curve-wrap svg { width: 100%; height: auto; display: block; }
#bdt-curve-path { stroke: #000; stroke-width: 2; }
&lt;/style&gt;

&lt;script src=&quot;/assets/js/bitdepth-toy.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;Aside from a straightforward loss of information, the result is sensitive to where you set the threshold.  If you set it wrong, you can completely miss what you’re looking for.&lt;/p&gt;

&lt;p&gt;If you want to see how far even just a little information can go, try “4-bit” mode where there are 16 distinct values that a pixel can have.  The image should be quite clear to your eye.&lt;/p&gt;

&lt;p&gt;Finally, try switching to the sigmoid mode.  If you set “steepness” very low, you should be able to see the flower pretty clearly no matter how you move “center”.  However, with the steepness set to a low number, you can see there’s not much contrast in the image.&lt;/p&gt;

&lt;p&gt;Start to crank up the steepness, and you’ll get contrast – but now it’s much more important where “center” is set.&lt;/p&gt;

&lt;p&gt;Trying to view a trend in this podcast data is a little bit like trying to determine which regions of that image are the flower versus the background.  It’s hard to trace the outline of the flower with 1-bit per pixel.  It’s a bit easier with more information.  It’s even easier if you set center=95 and steepness=50, amplifying the differences that separate the subject from the background.  That’s essentially what the sigmoid function is doing when applied to the similarity scores of each chunk of podcast content.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
    &lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
          There&apos;s no formal bound for the loss of recall performance with HNSW compared to an exact scan, but there&apos;s a generally established view that it works well in practice, and, of course, you can measure an epsilon with respect to your data.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
			Thinking about it a bit more deeply, `ef_search` (an HNSW parameter applied per query) needs to increase as `k` increases to maintain similar result quality.  This definitely costs latency, at least linearly in proportion to the value.  I&apos;m fairly certain there&apos;ll also be a sharp nonlinearity somewhere: above some value of `ef_search`, important bits of query state will start falling out of the CPU cache.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
        &lt;p&gt;
			Of course, there may be some datasets and some types of filters where this assumption holds.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Podcasts - Filtering Advertisements from Transcripts</title>
   <link href="https://taf.codes/2026/04/14/podcasts-1-filtering-ads.html"/>
   <updated>2026-04-14T00:00:00-04:00</updated>
   <id>https://taf.codes/2026/04/14/podcasts-1-filtering-ads</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
The podcast content I’ve been indexing was diluted with advertisements.  I’ve eliminated the majority of them fairly quickly with a basic prompt-based classifier.&lt;/p&gt;

&lt;h3 id=&quot;design-considerations&quot;&gt;Design Considerations&lt;/h3&gt;

&lt;p&gt;The main factors in play here are my time, inference costs, and the filter’s performance in terms of precision/recall.&lt;/p&gt;

&lt;p&gt;If I were aiming for the absolute best performance possible from an LLM-based classifier, without regard for my time or the inference cost, I would probably intervene before my chunking process.  At that point, any given podcast is a list of timecoded speech segments, like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4324.08&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4326.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Buy a product from one of our wonderful sponsors today.&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4327.08&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4330.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;We&apos;re back!  The largest squid can weigh in excess of 1000 pounds,&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;These segment boundaries impart non-negligible information.  They correlate strongly with pauses and transitions between different speakers.  They’re also more finely grained than my (~512 token) chunks.  I am fairly certain that I could get an LLM to process sequences of segments and, from them, extract the ranges that are advertisements.  I am uncertain how much this would cost, though.  I think it would require a larger model, more thinking/reasoning tokens, and more time spent on human labeling than the path I took instead.&lt;/p&gt;

&lt;p&gt;My actual solution involves no thinking/reasoning tokens, and works well with very small models (2 - 4 billion parameters).  Small is economical to begin with, and operating without thinking/reasoning means I can set the maximum output tokens to near-zero.  That’s neat, because then the total cost is essentially just input tokens and can be trivially estimated.&lt;/p&gt;

&lt;p&gt;To make the most efficient use of my time, I also wanted to use my existing tools wherever possible, and create a labeling process that I could move through quickly.  I decided to operate on a per-chunk basis, so every chunk is treated as either “mostly advertisements, nothing important” or “important content”.  This made &lt;a href=&quot;#labeling-setup&quot;&gt;labeling&lt;/a&gt; a breeze, and saved development time by letting me use my existing chunk-based filtering and “llm-map” tool, a straightforward batch prompt runner that just applies a prompt to a list of input texts and returns a result for each input.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;h3 id=&quot;howd-it-go&quot;&gt;How’d it go?&lt;/h3&gt;

&lt;p&gt;This strategy worked even better than expected.  With a considered (but not optimized) prompt, I evaluated performance across a few small models.  I threw Claude Sonnet into the ring as well, just out of curiosity.  Below is a &lt;a href=&quot;#comparing-models&quot;&gt;summary of their performance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That summary was produced from simply running my chunk-based filtering on a random sample of chunks, then exporting those chunks into label-studio, annotating all of them, and exporting the full dataset with human and machine labels for analysis.&lt;/p&gt;

&lt;p&gt;I chose &lt;a href=&quot;https://huggingface.co/Qwen/Qwen3.5-4B&quot;&gt;Qwen3.5-4b&lt;/a&gt;, which arguably outperformed Claude Sonnet 4.6 on this task, for ¹⁄₁₀₀&lt;sub&gt;&lt;sub&gt;th&lt;/sub&gt;&lt;/sub&gt; the price.&lt;/p&gt;

&lt;h3 id=&quot;any-other-cool-ideas&quot;&gt;Any other cool ideas?&lt;/h3&gt;

&lt;p&gt;Some podcasts include transcripts on the episodes’ landing pages.  It has occurred to me that these transcripts will probably be free of dynamically-injected advertisements.  Possibly these ad-free transcripts could be used in conjunction with my ad-containing transcripts to create labeled data automatically, with no human input.&lt;/p&gt;

&lt;p&gt;I’d also like to try using prompt optimization techniques (such as &lt;a href=&quot;https://dspy.ai/api/optimizers/GEPA/overview/&quot;&gt;GEPA&lt;/a&gt;) where I could experiment with guiding feedback.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h3&gt;

&lt;p&gt;While I was working on this, the system has continued to ingest ever more podcasts.  It should be possible to visualize some trends in the data very soon!&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;hr /&gt;
&lt;h3 id=&quot;build-log&quot;&gt;Build Log&lt;/h3&gt;

&lt;h3 id=&quot;labeling-setup&quot;&gt;Labeling Setup&lt;/h3&gt;

&lt;p&gt;I enriched&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;  my sample with ads to make labeling more efficient.  In my feeds, it seems like ads are roughly 10% of the total chunks, and I wanted to label at least a few dozen ‘ad’ chunks without labeling several dozen extra ‘content’ chunks unnecessarily.&lt;/p&gt;

&lt;p&gt;Labeling on a chunk basis was quick.  I have listened to a lot of podcasts, so I know how the ads typically read, and I know that they are usually above a certain length.  Thus, biasing my attention heavily towards the first, last, and middle sentences of a chunk gave me good signal very quickly, and made it possible to finish labeling in just a couple hours.  As a post-hoc validation, I gave special attention to cases where the models disagreed with my label and double-checked that I had not made any mistakes.&lt;/p&gt;

&lt;h3 id=&quot;inference&quot;&gt;Inference&lt;/h3&gt;

&lt;p&gt;I used Qwen3.5-2b locally via vLLM and was very pleased with its performance considering its size.  If pursuing maximum cost savings at scale, I’d dig a little deeper into trying to optimize its performance.  It could be an excellent case to try out &lt;a href=&quot;https://github.com/stanfordnlp/dspy&quot;&gt;DSPy&lt;/a&gt;’s prompt optimization.&lt;/p&gt;

&lt;p&gt;For the other models, I used DeepInfra.  I’m overall decently happy with them, though I have been getting a lot of transient 500 codes from their API (for minutes at a time).  On batch work like this, they’re still averaging ~15-20k TPS, so it’s fine I guess.&lt;/p&gt;

&lt;p&gt;It was tempting to run this locally, but I’m already tying up my server’s GPU with transcriptions for a large part of the day, so I was glad to do something that can run concurrently offsite.  DeepInfra doesn’t give me preferential pricing on Qwen3.5-4b for cacheable prefixes however, so I might switch to using ephemeral vLLM instances on RunPod GPUs, where I would be able to take advantage of the caching.&lt;/p&gt;

&lt;h3 id=&quot;comparing-models&quot;&gt;Comparing Models&lt;/h3&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;================================================================================
PER-MODEL METRICS (positive class = advertisement)
================================================================================
Total items: 189 | Human labels: 147 content, 42 advertisement

Model               Acc   Prec    Rec     F1   TP   FP   TN   FN  Err
---------------------------------------------------------------------
qwen3.5-2b        78.8%  51.7%  71.4%  60.0%   30   28  119   12    0
qwen3.5-4b        94.2%  94.3%  78.6%  85.7%   33    2  145    9    0
qwen3.5-9b        88.4%  67.9%  90.5%  77.6%   38   18  129    4    0
glm-4.7-flash     81.0% 100.0%  14.3%  25.0%    6    0  147   36    0
sonnet-4.6        87.3%  67.3%  83.3%  74.5%   35   17  130    7    0

================================================================================
PAIRWISE AGREEMENT (%)
================================================================================
                   qwen3.5-2b   qwen3.5-4b   qwen3.5-9b glm-4.7-flas   sonnet-4.6        human
qwen3.5-2b             100.0%        81.5%        79.9%        72.5%        75.7%        78.8%
qwen3.5-4b              81.5%       100.0%        88.9%        84.7%        85.7%        94.2%
qwen3.5-9b              79.9%        88.9%       100.0%        73.5%        76.7%        88.4%
glm-4.7-flash           72.5%        84.7%        73.5%       100.0%        75.7%        81.0%
sonnet-4.6              75.7%        85.7%        76.7%        75.7%       100.0%        87.3%
human                   78.8%        94.2%        88.4%        81.0%        87.3%       100.0%&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
    &lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
        	Using this component now will allow my ad-filtering process to benefit later, if/when I extend my llm-map to support the use of GPU spot instances.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
          This was another good task for claude-code subagents.  I don&apos;t expect the output to be correct.  I merely expect, on average, that it will be more effective than having done nothing.
      &lt;/p&gt;
      &lt;p&gt;
		In practice, this assumption seems to have been plausible.  After labeling, my sample was ~20% ads instead of only ~10%.  I could have gone higher, but I still wanted to label plenty of &quot;content&quot; chunks to be sensitive to false positives.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Searching Podcasts - The First Step Towards Trends Analysis</title>
   <link href="https://taf.codes/2026/04/05/podcasts-0-ingestion-and-search-interface.html"/>
   <updated>2026-04-05T00:00:00-04:00</updated>
   <id>https://taf.codes/2026/04/05/podcasts-0-ingestion-and-search-interface</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I’ve been processing podcast audio to build a semantic index.  Currently my index has limited coverage over a subset of popular podcasts.  I’m working towards trend analysis tooling, and have started by making a search frontend.&lt;/p&gt;

&lt;h3 id=&quot;how&quot;&gt;How?&lt;/h3&gt;
&lt;p&gt;Springboarding off of some tools I created &lt;a href=&quot;/2026/01/01/intake-and-embed-3.html&quot;&gt;earlier&lt;/a&gt;, I made a podcast tracker, started tracking ~100 popular feeds&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; , and wired up an &lt;a href=&quot;#podcast-tracking-and-ingestion&quot;&gt;ingestion pipeline&lt;/a&gt;.  This produces many segments of podcast content and their corresponding embedding vectors.&lt;/p&gt;

&lt;p&gt;That data is pushed into Postgres, where it’s indexed.  That vector index is enough to drive core similarity-search functionality.  I say &lt;em&gt;core&lt;/em&gt; functionality because this does not include re-ranking nor any lexical searching.  I’m not currently aiming at making a good search or RAG system, rather I’m trying to validate and understand the primitive (similarity) that I will use for analyzing trends.&lt;/p&gt;

&lt;p&gt;I have been wanting to try &lt;a href=&quot;https://hexdocs.pm/phoenix/overview.html&quot;&gt;Phoenix&lt;/a&gt;, so I used that to build a search interface:
&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;../../../assets/img/podcasts-0/search-construction.png&quot; alt=&quot;Search results for &apos;how to improve your deadlift&apos;, including segments from Rogan, Huberman, and one titled &apos;Get into Strength training in 2026&apos;.&quot; style=&quot;margin-left:5em; width:60%; border: 0.25rem solid; border-color: #3f3c8a;&quot; /&gt; &lt;br /&gt;
As you can see, I’m getting results roughly in line with what one might expect from such a system.  The podcast segments from the example can be listened to at these links: &lt;a href=&quot;https://omny.fm/shows/odd-lots/how-insurance-costs-make-nyc-construction-so-expensive?t=3m&quot;&gt;[1]&lt;/a&gt; &lt;a href=&quot;https://omny.fm/shows/odd-lots/why-its-still-so-expensive-to-build-homes-in-america?t=19m30s&quot;&gt;[2]&lt;/a&gt;.  If you’re curious, you can &lt;a href=&quot;#search-application&quot;&gt;read more about how this search works&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h3&gt;

&lt;p&gt;I’ve been running a lot of queries and have found that my results are frequently compromised by advertisements.  Certain topics (those which tend to be most strongly associated with advertising campaigns) are essentially unsearchable.  In other cases, the ads will merely dilute otherwise decent results.&lt;/p&gt;

&lt;p&gt;Now that I think of it, it’s actually interesting to think about trends in advertisements&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; , but that’s not what I want to look into right now.  I’m going to try and do some filtering to mitigate their impact on my index.&lt;/p&gt;

&lt;p&gt;After that, I’ll build some trend analysis features.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;build-log&quot;&gt;Build Log&lt;/h3&gt;
&lt;p&gt;&lt;sup&gt;&lt;sup&gt;(Skip this part unless you’re interested in the details.)&lt;/sup&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h4 id=&quot;podcast-tracking-and-ingestion&quot;&gt;Podcast Tracking and Ingestion&lt;/h4&gt;
&lt;p&gt;This is managed by a simple app that tracks state in sqlite.  Most operations it carries out are compositions of my existing (stateless) utilities.&lt;/p&gt;

&lt;p&gt;Feeds are synchronized daily to track new episode releases.  I noticed that some of these RSS/Atom feeds are large-ish (tens of MB), so I added some support for conditional request headers.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;  This is mostly out of an abundance of consideration for the hosts of this data, but it could also allow me to sync more often.&lt;/p&gt;

&lt;p&gt;As new episodes are discovered, they are downloaded.  I’ll grab up to a few from each feed each night, which is enough to keep me current and to start working through the backlog of older episodes.&lt;/p&gt;

&lt;p&gt;Downloaded episodes are transcribed using Whisper (v3-large-turbo).  For clean, professionally recorded audio, I could not find substantial differences between v3-large and v3-large-turbo, and with turbo I can run decent batch sizes on my local GPU.  Running this myself actually provides a substantial cost savings compared to using a transcription API.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;The generated transcriptions include timecode information.  The breaks here often coincide with natural breaks in speech or pauses between speakers, so I use that information when chunking the text.&lt;/p&gt;

&lt;p&gt;After this, I apply chunk-level filters.  These are duration aware, so I can remove stuff like the odd chunk that may be leftover at the very end of an episode, is 5 seconds long, and just says “Thanks for listening!”.  I also shoehorned some ad-filtering in at this stage, but more about that in &lt;a href=&quot;/2026/04/14/podcasts-1-filtering-ads.html&quot;&gt;my next post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All remaining chunks are embedded.  Then I push data to the Elixir/Phoenix project where it’s indexed.&lt;/p&gt;

&lt;h4 id=&quot;search-application&quot;&gt;Search Application&lt;/h4&gt;

&lt;p&gt;I’m really enjoying Phoenix so far.  It was almost disappointing how well-structured it is, in that I had been expecting and looking forward to writing a bit more Elixir.  For the first couple hours, I was skeptical about the amount of macros involved, but, in practice, everything seems to be assembled thoughtfully enough that one may not need to look behind them often.&lt;/p&gt;

&lt;p&gt;For basic similarity search, the only things I needed beyond basic web-framework stuff were &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgvector&lt;/code&gt; for Postgres, and some way to compute the query embeddings.  Tempting though it was to compute them on the server and maintain full custody of the essential infrastructure, I was not about to rent a cloud GPU full-time nor even use a VPS with enough RAM to hold the weights of the embedding model.  The second temptation was to do something serverless (or at least dynamic), but, with no volume, every visitor would hit cold-start latency.  So I went with DeepInfra’s API, which seems to be performing acceptably.&lt;/p&gt;

&lt;p&gt;Search is driven entirely by &lt;a href=&quot;https://en.wikipedia.org/wiki/Nearest_neighbor_search#Approximate_nearest_neighbor&quot;&gt;ANN&lt;/a&gt; operations in semantic embedding-space, so it behaves differently than keyword or text search.  In general, it performs well at finding plausible results based on real meaning.  None of the words in the query need to match any of the words in the content to find a match.  In contrast, it cannot perform at all if you &lt;em&gt;do&lt;/em&gt; want textual matches, such as names of individual human persons.&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
    &lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
          I used claude-code to pick feeds, which is to say that I exerted fairly little judgement.  Ultimately, I just wanted a sample that skewed heavily towards the most popular feeds but which would be somewhat depleted of &quot;chat shows and celebrity stuff&quot;, as I called them.
        &lt;/p&gt;
        &lt;p&gt;
          Using Apple&apos;s charts of top podcasts as a starting point, I had claude fetch and parse feeds and their descriptions, and then dispatch subagents to do some ad-hoc classification to the effect of, essentially, &quot;substantive content&quot; vs &quot;fluff&quot;.  I had a much more detailed prompt than that, of course, but I&apos;m not really counting on accuracy/adherence here.
        &lt;/p&gt;
        &lt;p&gt;
          Essentially, I know from just a cursory glance that this ad-hoc classification without human feedback has made misclassifications compared to what I would have done manually, but I&apos;m &lt;i&gt;also&lt;/i&gt; pretty confident that the net effect is that my sample of feeds is closer to what I had wanted than if I had done no filtering at all.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
          If I did want to look into trends in advertising, I would have to alter my methodology.  Ads are injected dynamically at the time of download, &lt;i&gt;not&lt;/i&gt; always present when the episode is published.  So, if you wanted to watch advertising change over time, you&apos;d want to be thinking about the time the ad was served rather than the time the podcast was published.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
        &lt;p&gt;
          I&apos;m using HTTP &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-Modified-Since&quot;&gt;&quot;If-Modified-Since&quot;&lt;/a&gt;.  Empirically, it looks like ~50% of feeds support this.  This may seem like a small thing, but consider fetching a podcast feed on a daily basis that only updates once per week -- 85% of all downloads can be replaced with &lt;code&gt;HTTP 304 Not Modified&lt;/code&gt; responses!  If you wanted to synchronize feeds hourly, then only 1 out of every 168 fetches would download anything.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
        &lt;p&gt;
          This might surprise you if you have been paying attention to how highly commoditized LLM inference is these days.  It&apos;s true that, for LLM operations, I cannot typically beat the prices of major providers (especially on my last-gen hardware which offers less FLOPs/Watt efficiency).  However, transcription is a different story.  My impression is that STT API providers are aiming more at low-latency to support customers trying to run realtime voice interfaces, and maybe they aren&apos;t focusing as much on batch workloads.  Whatever the reason, the upshot is that it pays to be close to the hardware here, and it&apos;s more than enough to offset the engineering time.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
        &lt;p&gt;
           Interestingly, that&apos;s not &lt;i&gt;uniformly&lt;/i&gt; true.  If a person is sufficiently famous and is often written about, they may actually have a large enough foothold in semantic space that references to them are effectively disambiguated.
        &lt;/p&gt;
        &lt;p&gt;
          The same is true of other proper nouns.  For example, Nvidia, currently the largest company in the world, can be referenced by name.  Essentially, the embedding model &quot;knows what Nvidia is&quot;, to anthropomorphize.  Keep working your way down a list of companies sorted by market capitalization however, and before long it will likely be impossible to query by name.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Logseq and Continuous Backups</title>
   <link href="https://taf.codes/2026/02/08/logseq-and-continuous-backups.html"/>
   <updated>2026-02-08T00:00:00-05:00</updated>
   <id>https://taf.codes/2026/02/08/logseq-and-continuous-backups</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I’ve switched my personal notes over to Logseq, even though I’ve heard complaints it can lose data.  Here’s a continuous-backup wrapper script you can apply to any program that loses your work.&lt;/p&gt;

&lt;h3 id=&quot;why-logseq&quot;&gt;Why Logseq?&lt;/h3&gt;
&lt;p&gt;I found the backlinks and transclusions&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;  compelling.  Linking and embeds/transclusions are not only for pages, these can be applied to individual “blocks”.  Frequently I want the same information to be accessible from multiple places, and Logseq has a good UI for this.  An interlinked network of text-blocks just makes sense to me.&lt;/p&gt;

&lt;p&gt;I know that Roam Research and some others have similar features, but Logseq is fully FOSS.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;h3 id=&quot;why-not-logseq&quot;&gt;Why Not Logseq?&lt;/h3&gt;

&lt;p&gt;These notes are fairly important and contain my own personal documentation for how I manage pieces of the infrastructure that keeps me functioning.  Meanwhile, I have seen various complaints across the internet about Logseq losing data or behaving in confusing ways.  The safest thing to do would be to use a simple text editor that is nearly incapable of losing important information.&lt;/p&gt;

&lt;h3 id=&quot;using-unpredictable-software-anyway&quot;&gt;Using Unpredictable Software Anyway&lt;/h3&gt;

&lt;p&gt;Of course, using a simple text editor, I’d miss out on the features I want.&lt;/p&gt;

&lt;p&gt;One nice thing about Logseq is that it stores data in a bunch of markdown files, so edits are completely transparent to a casual observer.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Given this highly-legible backing store, it’s almost completely trivial to mitigate any concern about data loss.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;h4 id=&quot;automatic-snapshots&quot;&gt;Automatic Snapshots&lt;/h4&gt;

&lt;p&gt;At first I’d considered taking snapshots of my notes every minute or so, but this would needlessly churn the disk.  Then I remembered that change detection is easy with Inotify.  Inotify is one of my favorite tools - you can use it to track filesystem events and notify a user process.&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;If you want to see what it does, you can install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inotify-tools&lt;/code&gt; (almost certainly available in most Linux distros) and run something like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;inotifywait &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; modify,create,delete,moved_to &lt;span class=&quot;nv&quot;&gt;$SOME_DIRECTORY&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#this filters for write events, but you can monitor read-only events too!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That will show you all changes to any files or folders within the given directory, streaming to your terminal.&lt;/p&gt;

&lt;p&gt;Monitoring changes in this way, it’s easy to take a snapshot in response.&lt;/p&gt;

&lt;h4 id=&quot;why-not-git&quot;&gt;Why not git?&lt;/h4&gt;
&lt;p&gt;Logseq has some built-in automatic git-based versioning.  Semantically, this seems wrong to me (but that’s only because of my philosophy about notes).  I see notes as evolving but not really “versioned” in the same way that source code is.  I see git commits as, ideally, meaningful and capturing an atomic intention.  Automating commits separates them from any user intention.  Of course, git doesn’t care, and it would work fine.&lt;/p&gt;

&lt;p&gt;Another reason to build a wrapper is to avoid trusting Logseq to make its own backups.  I haven’t read the code, I don’t know what it’s doing in there.  Meanwhile, I know exactly what my wrapper does.&lt;/p&gt;

&lt;h3 id=&quot;wrapper-script&quot;&gt;Wrapper Script&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://forge.taf.codes/taf/snippets/src/branch/main/backups/wrap_untrusted_software_with_continous_backups__restic_and_inotify&quot;&gt;Here’s the script I have wrapped Logseq with.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It uses a tiny standalone restic repository, then takes snapshots on launch, on quit, and whenever changes have been made (but never more than one within 30 seconds).&lt;/p&gt;

&lt;p&gt;You can wrap any program with this script.  You’d still have to remember to save frequently, but, 30 seconds after it hits the disk, even the worst-behaved program in the world can’t nuke your work.&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;#fn:6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
          Transclusions are a way to use the same &apos;block&apos; or chunk of text in multiple places.
        &lt;/p&gt;
        &lt;p&gt;
          In some cases, a transclusion is preferable to copy/pasting a passage from one document into another.  There&apos;s actually only &lt;i&gt;one&lt;/i&gt; copy of the passage saved on disk.
        &lt;/p&gt;
        &lt;p&gt;
          For example, I have some notes about running an &lt;a href=&quot;https://en.wikipedia.org/wiki/Network_File_System&quot;&gt;NFS&lt;/a&gt; service on one of my servers.  This material can appear simultaneously in:
          &lt;ul style=&quot;margin-left: 2em;&quot;&gt;
            &lt;li&gt; A page about NFS generally, alongside any other information I need to retain about NFS.&lt;/li&gt;
            &lt;li&gt; A page about the specific server that runs this software service, inside a list of all the services running on that server. &lt;/li&gt;
            &lt;li&gt; A page about one of my other systems that connects to the NFS server as a client.&lt;/li&gt;
          &lt;/ul&gt;
        &lt;/p&gt;
        &lt;p&gt;
          It can appear in full-text in all of these places, be editable &lt;i&gt;anywhere&lt;/i&gt; I&apos;m looking at it, and always updated everywhere when I make changes to it.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
        	Of course, one might like to be aware that they&apos;ve taken some &lt;a href=&quot;https://blog.logseq.com/logseq-raises-4-1m-to-accelerate-growth-of-the-new-world-knowledge-graph/&quot;&gt;funding&lt;/a&gt;.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
        &lt;p&gt;
        	Unfortunately, &lt;a href=&quot;https://discuss.logseq.com/t/why-the-database-version-and-how-its-going/26744&quot;&gt;this may be changing&lt;/a&gt;.
        &lt;/p&gt;
         &lt;p&gt;
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
        &lt;p&gt;
        	Obsidian has this property too.  Good luck building your own guarantees on top of, say, Notion, though!
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
        &lt;p&gt;
        	 &lt;b&gt;TIL:&lt;/b&gt; Inotify works on &lt;a href=&quot;https://wiki.archlinux.org/title/FUSE&quot;&gt;FUSE&lt;/a&gt; filesystems!  I suppose I hadn&apos;t thought there was a strong reason it wouldn&apos;t, but if you&apos;d asked me about it before I&apos;d have probably emitted a long drawn-out, thoughtfully uncertain &quot;hmmmmm&quot;.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
        &lt;p&gt;
        	Unless it knows where the backups are stored and deletes them. ;)
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Document Intake and Embedding: Millions of Tokens from Audio -- Part 3</title>
   <link href="https://taf.codes/2026/01/01/intake-and-embed-3.html"/>
   <updated>2026-01-01T00:00:00-05:00</updated>
   <id>https://taf.codes/2026/01/01/intake-and-embed-3</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I transcribe audio to get ~20 million words of text, then load this into my ‘knowledgebase system’ with several different chunking configurations and embedding models.&lt;/p&gt;

&lt;h2 id=&quot;how&quot;&gt;How?&lt;/h2&gt;
&lt;p&gt;For me, the main headline here was that all of this runs faster than expected.&lt;/p&gt;

&lt;p&gt;In short, using an RTX 3060 with flash-attention:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;You can generate pretty solid transcriptions of audio at ~100x realtime, and&lt;/li&gt;
  &lt;li&gt;embed text at 2,000 tok/s, even with a (large-ish) 4B embedding model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just thinking about it naively without calculating anything, I had underestimated the gains available from batching on a small-ish GPU like this.  Considering the actual performance, I scaled up my experiment a bit.  Every document is getting embedded 10 times across the different models and chunking configs, so there’s ~200 million tokens going through the embedding pipeline, but that only took ~24 hours.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/2025/12/12/reading-intake-and-embed-2.html&quot;&gt;Previously&lt;/a&gt;, I observed excellent retrieval behavior from a rather small set of documents, ~500k tokens.  Now, with 40x as much text in the system, will retrieval start to fall off, qualitatively?&lt;/p&gt;

&lt;h2 id=&quot;analysis-same-knowledgebase-more-documents&quot;&gt;Analysis: Same Knowledgebase, More Documents&lt;/h2&gt;

&lt;h3 id=&quot;effects-of-collection-scale-on-retrieval-of-specific-facts-known-to-be-in-the-collection&quot;&gt;Effects of Collection Scale on Retrieval of Specific Facts Known to Be in the Collection&lt;/h3&gt;
&lt;p&gt;I have a very basic rubric which attempts to retrieve specific facts from the knowledgebase.  The grading is based on whether a given answer string is contained in the returned chunks, and how those chunks rank.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Query: “What’s a tool that can help me identify keypresses on
 Linux?”&lt;/li&gt;
  &lt;li&gt;Answer: must contain “xev”, and be from a certain document.&lt;/li&gt;
  &lt;li&gt;Score: 5 points if it’s the first ranked result, 4 points if it’s the second ranked result, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding 40x more documents to my collection doesn’t meaningfully impact the retrieval of that particular fact.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Of course, one should ask at this point &lt;em&gt;where&lt;/em&gt; in semantic space I added all these new documents!  If you had one document about handling keypresses in Linux and then added a billion documents about baseball, this result would be uninformative.  In this case, there’s some technical content mixed into the corpus, but I can’t rigorously quantify the amount at the moment.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Aside from that, there are serious limitations to this analysis.  First of all, I have chosen correct answers ahead of time based on some needles I happened to know were buried in the haystack.  With this current method, a retrieval function would score zero points for returning a chunk mentioning &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wev&lt;/code&gt;, which is another tool that might help me identify keypresses on Linux.&lt;/p&gt;

&lt;p&gt;Another less immediately obvious limitation is that, since I am experimenting with a range of chunk sizes, these scores are biased in favor of small chunks when the passage the chunks came from contained multiple instances of the answer string.  (There are more right answers when there are more matching chunks.)&lt;/p&gt;

&lt;p&gt;For now, I’m mostly just trying to gain some first-hand appreciation of working with knowledgebase systems, so I’m happy to have even the barest wisps of signal to investigate.  One thing which my current, weak methodology doesn’t overtly preclude me from attempting is comparison of different models’ behavior with the same chunking configuration.&lt;/p&gt;

&lt;p&gt;Some signals of interest:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Smaller or lower dimensionality models seem to be more affected by the increased size of the collection.&lt;/li&gt;
  &lt;li&gt;Larger models seem to perform better, but this effect is not uniform across tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither of these is at all surprising, but it’s nice to see that I’m turning up what you’d expect!&lt;/p&gt;

&lt;p&gt;I’m very interested in coming up with a believable, qualitative description of the types of tasks where larger models perform meaningfully better, and hopefully one that I can at least somewhat substantiate.&lt;/p&gt;

&lt;h3 id=&quot;vibes-about-model-size&quot;&gt;Vibes About Model Size&lt;/h3&gt;

&lt;p&gt;One query I tried side-by-side with a small model and a large model was: “What type of insurance is most profitable for the insurer?”.&lt;/p&gt;

&lt;p&gt;The smaller model just retrieved a bunch of advertisements about insurance.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;   The larger model retrieved precisely the passage I was looking for, one in which the speaker describes the loss-ratios of different types of insurance products.&lt;/p&gt;

&lt;p&gt;Does the larger model include concepts such as the relation between this “loss ratio” and the profitability of a given insurance product?
I don’t strictly know.&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Another one I tried side-by-side, after randomly selecting a document containing information about porcelain: “What is a type of earthenware material that’s harder than most?”.&lt;/p&gt;

&lt;p&gt;The smaller model returns 5 passages about granite, metal alloys, titanium, and fiberglass.  A medium-sized model returns 3 passages about porcelain and a couple about rocks and minerals.  The larger model returns 5 passages in a row about porcelain.&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;#fn:6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Ultimately, these are just vibes, but I feel like I’m starting to get a sense for the kinds of concepts which might exist in larger models but might not exist (or might be poorly defined) in smaller ones.&lt;/p&gt;

&lt;h3 id=&quot;on-retrieval-relevance&quot;&gt;On Retrieval Relevance&lt;/h3&gt;

&lt;p&gt;One of my benchmark queries (scored as described above) &lt;em&gt;was&lt;/em&gt; seriously affected by adding more documents to the system.  This was almost certainly an artifact of scoring based on a rubric.&lt;/p&gt;

&lt;p&gt;A rubric makes the assumption that we know every correct answer ahead of time.  I violated this assumption by adding hundreds of new documents to the corpus without explicitly reviewing every one to find every possible relevant passage for every query in the rubric.&lt;/p&gt;

&lt;p&gt;In reality, the newly added documents happen to contain information relevant to the query, and these new passages are being retrieved (just as you would expect from a system that’s functioning properly).&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;really&lt;/em&gt; want to do is measure the relevance of the retrieved passages with respect to the query.  What I &lt;em&gt;am&lt;/em&gt; measuring is only whether the ‘right’ passages (defined as the relevant passages which I already know about) are retrieved.  Now there are multiple ‘right’ passages, and I don’t know about all of them.&lt;/p&gt;

&lt;p&gt;How do you benchmark the retrieval performance of a system when you don’t know all the answers?&lt;/p&gt;

&lt;p&gt;I have some thoughts about this&lt;sup id=&quot;fnref:7&quot;&gt;&lt;a href=&quot;#fn:7&quot;&gt;7&lt;/a&gt;&lt;/sup&gt; , but, frankly, I’ve reached a point in this where it’s probably time for me to review the literature.&lt;/p&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;Other than hitting up Arxiv, I’m starting to think of quality-of-life improvements for working with this (and related) systems.  Multiple times in this project it would have been handy to have a priority queue setup to manage the GPU workload.  I keep thinking there must be some way to approximate pre-emptive scheduling (coarsely), but it may very well be more trouble than it’s worth.&lt;/p&gt;

&lt;p&gt;I’m developing a composable system of tooling where each tool is specifically designed to be maximally legible to claude-code.  The idea is many small and relatively stateless tools which are each available through a CLI, a JSON-RPC interface, and as a Python library.  Ultimately, I want to be able to use my Python utilities from Elixir, which motivated the RPC interface.&lt;/p&gt;

&lt;p&gt;For example, I implemented the batched transcription part of this project as a tool, since it will be useful in more than one place.  If I’m using another Python project, I can install the tool as a library in that project.  The same tool could be used in an Elixir project via the JSON-RPC interface over a Unix domain socket.  It can also be used from the shell to do a one-off transcription of a folder full of files, or for testing, or for throwing together a quick job written in Bash.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
          Yes, I know OpenAI will do it for ~$26.  My knowledgebase experiments are decidedly local-first though.  It would be pretty annoying to take time exploring all the characteristics of their embeddings models and then go on to build a system using them, only to have them be superceded or discontinued.
        &lt;/p&gt;
        &lt;p&gt;
          Interestingly, they don&apos;t mention text embeddings at all on their &lt;a href=&quot;https://openai.com/api/pricing/&quot;&gt;pricing page&lt;/a&gt;.  If you click through to &quot;detailed pricing&quot;, you might notice that &quot;Embeddings&quot; is way down the page next to undesirables like &quot;Moderation&quot; and &quot;Legacy Models&quot;.  While it might be foolish to read too far into that, I was already disinclined to develop any reliance on closed-weights models, particularly in a tool that other systems might be built on top of.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
        	On a per-query basis, adding 40x more documents to the collection doesn&apos;t change the benchmark results much.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
        &lt;p&gt;
        	Essentially, we are talking about estimating the density or sparsity of points in subregions of the high-dimensional space where our texts are embedded.
        &lt;/p&gt;
         &lt;p&gt;
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
        &lt;p&gt;
        	One suggestion I&apos;ve heard but cannot vouch for (although it feels at least kind of truthy) is that smaller models may, to a degree, conflate words like &quot;profitable&quot; with &quot;good&quot; generally.  The insurance advertisements did indeed use a lot of positive words about the quality of their insurance, so, maybe?
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
        &lt;p&gt;
        	In this case, the embedding model was Qwen3-Embedding-4B.  That&apos;s derived from Qwen3-4B.  You can download this model and chat with it.  So I asked it: &quot;What determines the profitability of a given insurance business?&quot;.  &lt;a href=&quot;https://gitlab.com/-/snippets/4922307&quot;&gt;It mentions &quot;loss ratio&quot; as the first &quot;Core Profitability Metric&quot;.&lt;/a&gt;  That seems kind of suggestive.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
        &lt;p&gt;
        	I don&apos;t think we should imagine that there is any remotely linear correlation between model size and the presence of any given concept, but it is kind of interesting that this n=1 example appears that way.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
        &lt;li id=&quot;fn:7&quot;&gt;
        &lt;p&gt;
        	In essence, the cybernetic principle.  &lt;i&gt;Some&lt;/i&gt; kind of feedback.  Perhaps a task-specific fitness function, rather than attempting to measure &apos;retrieval relevance&apos;.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:7&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Document Intake and Embedding for Semantic Search -- Part 2</title>
   <link href="https://taf.codes/2025/12/12/reading-intake-and-embed-2.html"/>
   <updated>2025-12-12T00:00:00-05:00</updated>
   <id>https://taf.codes/2025/12/12/reading-intake-and-embed-2</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I implemented semantic search over &lt;a href=&quot;/2025/10/30/reading-intake-and-embed-1.html&quot;&gt;a collection of documents I had handy&lt;/a&gt;.  In short, it works too well!  My benchmark is saturated; every model does nearly perfectly.  Now I want to find out where they &lt;em&gt;do&lt;/em&gt; differ in performance.&lt;/p&gt;

&lt;h2 id=&quot;why&quot;&gt;Why?&lt;/h2&gt;

&lt;p&gt;I’m interested in knowledgebase systems.&lt;/p&gt;

&lt;h3 id=&quot;what-is-a-knowledgebase&quot;&gt;What is a knowledgebase?&lt;/h3&gt;
&lt;p&gt;If you ask me, a knowledgebase is simply a collection of documents and some sort of index mechanism which allows them to be selectively retrieved by their relevance to a given objective.  Critically, “a collection” implies curation.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;By this definition, a library might count as a knowledgebase,&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;  although the index is somewhat coarse-grained.  Let’s consider how you might use a library to further develop an idea about what a theoretically ideal knowledgebase might look like.&lt;/p&gt;

&lt;div class=&quot;inset6&quot;&gt;

  &lt;p&gt;&lt;strong&gt;Case Study: Finding Information at the Library&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;Suppose you wanted to learn about the main exports from a given geographical region, both current and historical, and develop a decently detailed picture of how trade has shaped the economy of the region over time.  Let’s consider how you might use the library:&lt;/p&gt;

  &lt;p&gt;One approach might be to use the Dewey Decimal system.  Here are some areas that might contain books that might contain the information you’re looking for:&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;&lt;em&gt;330s&lt;/em&gt; – Economics generally&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;382&lt;/em&gt; – International commerce, specifically trade and exports&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;338.1&lt;/em&gt; – Agricultural products and their economics&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;338.2&lt;/em&gt; – Extraction of minerals and other natural resources&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;338.4&lt;/em&gt; – Secondary industries and services&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;339&lt;/em&gt; – Macroeconomics, which sometimes covers trade balances&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;910s&lt;/em&gt; – Geography, travel, and regional descriptions (often includes economic geography)&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;330.9 + region code&lt;/em&gt; – Economic history and conditions by place (e.g., 330.94 for Europe)&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;Another approach might be to search the catalog by keyword.  Additionally, you might look to atlases or almanacs like the CIA World Factbook, but these would only give you higher-level summary information.&lt;/p&gt;

  &lt;p&gt;However you do it, you’ll end up with perhaps dozens of books.  Some of these may have their own internal indexes, some may not.  Of course, there could be a great value in reading (or even just skimming through) these dozens of books, but usually our queries are bounded.  We might want to know more than a factbook or atlas could tell us, but not necessarily have use for dozens of books’ worth of adjacent information.&lt;/p&gt;

&lt;/div&gt;

&lt;h4 id=&quot;efficiently-retrieving-information-from-a-knowledgebase&quot;&gt;Efficiently Retrieving Information from a Knowledgebase&lt;/h4&gt;

&lt;p&gt;At this point, if you’re familiar with LLMs but don’t happen to know about embeddings, you might think of scanning the full content of every book with instructed chat sessions.  You could just chunk the book into sections, run a chat thread over each chunk, and have them all return essentially a classification: “relevant or not-relevant”.&lt;/p&gt;

&lt;p&gt;This would work!  Supposing there were 20 books of 50k words each, that’d be approximately 1 million tokens.  Running this scan would cost you something like $1, at 2025 prices.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;   It might take a while, depending on how bursty your inference provider is willing to let you be.&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;A more efficient strategy for semantic search might be to use text embeddings.  You could think of this as (&lt;em&gt;very approximately&lt;/em&gt;) like a similar LLM “reading” all those same chunks of text and then placing them into an index based on what they’re about.  Instead of an alphabetically-sorted keyword index like you might find in the back of a reference book, these indexes are more like a multi-dimensional “map”.&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;#fn:6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;In this case, the index only needs to be created once, and then it can be used for multiple, different queries.  Creating the index involves an LLM, and is computationally intensive, though it’s more efficient than dumping documents into chat threads.  In practical terms, you might imagine that all those Dewey Decimal areas above contain 5,000 books, or 250 million words.  This amount of material could be indexed for ~$25 in AI inference costs.  Then, if your typical query was ~100 words, you could run 100,000 queries for another $1 in AI inference costs.  Amortized, this is like $0.0003 per query.&lt;sup id=&quot;fnref:7&quot;&gt;&lt;a href=&quot;#fn:7&quot;&gt;7&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Running queries like this is also fast - you can pretty easily have results in less than a second!&lt;/p&gt;

&lt;h2 id=&quot;what-im-working-on&quot;&gt;What I’m Working On&lt;/h2&gt;

&lt;p&gt;I’ve continued working on my knowledgebase experiments.  Now that I have a collection of documents with which I have at least a passing familiarity, I’m well positioned to run queries on them and assess the vibes.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I email myself a URL, possibly with some metadata.&lt;/li&gt;
  &lt;li&gt;The URLs are fetched, PDFs are rendered for reading, and the text content is stored.&lt;/li&gt;
  &lt;li&gt;The documents are broken up into chunks.&lt;/li&gt;
  &lt;li&gt;Embedding vectors are computed for the chunks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The highlights here are that it’s very easy to experiment with different chunking strategies and with different embedding models.  They’re basically pluggable.&lt;/p&gt;

&lt;p&gt;If I wanted to, for example, experiment with context-enriched chunking&lt;sup id=&quot;fnref:8&quot;&gt;&lt;a href=&quot;#fn:8&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;, I could just write the new chunker and specify a config that uses it.  The new table will be automatically created in Postgres and it’ll automatically be populated with chunks on the next batch run.  Adding a new embedding model works similarly.  Once a new embedding configuration is registered, the schema will be automatically updated and all the embeddings will be computed on the next run.  The benchmark script will also be aware of the newly registered config.&lt;/p&gt;

&lt;h2 id=&quot;benchmark-saturation&quot;&gt;Benchmark Saturation&lt;/h2&gt;

&lt;p&gt;Okay, so my current benchmark is just some queries that I noted down while I was reading some of the articles I mailed myself.  I tried to write my queries such that they did not include any of the same high-entropy words as the passages/chunks I hoped they would retrieve, in order to stress the semantic representations.&lt;/p&gt;

&lt;p&gt;In short, all the models, even the smallest, retrieve exactly what my rubric expects.  So, if I want to learn about the relative capabilities of these models, I’ll have to think of something else to try.&lt;/p&gt;

&lt;h3 id=&quot;why-is-my-benchmark-saturated&quot;&gt;Why is my benchmark saturated?&lt;/h3&gt;
&lt;p&gt;I think I chose decently tricky queries that should only be answerable when there’s some degree of concept modeling present (rather than queries that you could just answer by doing text search, or a fancy text-search with a thesaurus).  I expected some to fail, but none did.  Why?  Are these embedding models just far better than I’d expected?&lt;/p&gt;

&lt;p&gt;One possibility is that there simply aren’t enough documents embedded in my database and so there’s very little possibility for confoundment.  That is to say, maybe these queries are actually casting a very wide net through semantic space, but there’s such a sparse distribution of passages in that space that even a wide net will still manage to catch the right passage (and even rank it as the best one), because there is nothing else to get swept up in the net.&lt;/p&gt;

&lt;p&gt;For example, I have one article in my collection that describes a text-based web browser that uses AI to rewrite pages.  When I search for “an app that lets me look at websites with a TUI”, well, there’s only a handful of articles in my collection that even mention browsers heavily in the first place.  So, even if the recall was relatively imprecise, it might still get the correct answer!  It could be that my collection of documents is sufficiently small that something like this happens for all of them.&lt;/p&gt;

&lt;h3 id=&quot;scaling-up-the-collection&quot;&gt;Scaling Up the Collection&lt;/h3&gt;

&lt;p&gt;It might be interesting to try indexing some substantially larger collections of documents.  However, this creates difficulties for a vibes-based assessment: ideally I have actually read all of these documents so that I have a good sense for what is present in the collection and what degree of confoundment might be expected.&lt;/p&gt;

&lt;p&gt;I can go and get a large corpus of material, but if I don’t read all of it, I cannot know if a given query returns all of the results that I would reasonably have expected it to return.  Certainly I can evaluate the results that &lt;em&gt;are&lt;/em&gt; returned and judge their relevance to the query, but I have no idea how many relevant results are &lt;em&gt;not&lt;/em&gt; being returned.&lt;/p&gt;

&lt;p&gt;Anyway, more documents seems like the best next move.  Even if I can’t be as rigorous as I’d like, I can still learn something about how recall performance is affected by the amount of material in the collection.&lt;/p&gt;

&lt;h2 id=&quot;some-other-ideas-knowledge-graph-traversal&quot;&gt;Some Other Ideas: Knowledge-graph Traversal&lt;/h2&gt;

&lt;p&gt;I include a document ID and document sequence number in my chunks tables because the sequence number turns the chunks into a very simple graph.  That is to say, you could follow the sequence number up or down to get additional context around any given passage.  Or an LLM could make a tool call to request this if you were doing RAG and the passage retrieved was relevant but insufficient.&lt;/p&gt;

&lt;p&gt;I’ve been thinking about also including references to other documents in the metadata if, for example, reading one somehow otherwise prompts me to discover the other.  That is probably too labor-intensive to track, though.  Another possibility which leverages my own judgements less (but which is accordingly easier), would be to just parse links in documents.  These could theoretically be crawled automatically.  Of course that eventually covers the whole Internet, so there’d need to be some rules to limit the depth, and I don’t know how much bandwidth I’d want to spend on that anyway.&lt;/p&gt;

&lt;p&gt;Wikipedia has plenty of internal links and is available as a bulk download, so using their data could sidestep these issues and might be a quicker way to create a toy knowledgebase for exploring graph features.&lt;/p&gt;

&lt;h2 id=&quot;code&quot;&gt;Code&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://gitlab.com/t-f/document_intake_and_embedding/&quot;&gt;Source on GitLab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
          In my manner of speaking then, the Internet isn&apos;t a knowledgebase.  It may be indexed, but it&apos;s not &quot;a collection&quot; in any coherent, intentional sense.  Admittedly, this distinction can rapidly become blurry if you consider different degrees and means of &quot;curation&quot;, like PageRank etc.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
          I would suggest that it depends on where your specific cutoff is for curation. To what degree do you consider every book in the library to be an authoritative source?
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
        &lt;p&gt;
          This list was suggested by Claude.  I don&apos;t happen to be all that proficient with the Dewey Decimal system, and I&apos;m presuming that these areas are not entirely ridiculous.  Maybe one could make a case to remove some of them, but my point is that there are at least a few different areas and a few different books in each.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
        &lt;p&gt;
          Okay, sure, it depends on which model you use.  Without trying to figure out what would be suitable for this use case, just know that a small model might be as cheap as 10¢ per million tokens, and an expensive one might be as much as $10 per million tokens.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
        &lt;p&gt;
          If you were limited to a single concurrent chat, it might take 3 hours from a traditional provider, or 15 minutes from Cerebras.  Though compute-intensive, a scan like this would be &lt;a href=&quot;https://en.wikipedia.org/wiki/Embarrassingly_parallel&quot;&gt;embarrassingly parallel&lt;/a&gt;, so you can just divide by how many concurrent chats you can run to get the wall time.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
        &lt;p&gt;
          In keeping with the case study involving geographically-related research, you could imagine a couple different scenarios:
        &lt;/p&gt;
        &lt;p&gt;
          a) Every paragraph of every book could be placed into a 1-dimensional, alphabetically-sorted index of place names.  All the paragraphs about Calgary would be listed under &quot;Calgary&quot;.  And &quot;Canada&quot; might have a much bigger list of paragraphs: anything that pertains to any place in Canada.
        &lt;/p&gt;
        &lt;p&gt;
          b) Every paragraph of every book could be indexed by giving it an area on a 2-dimensional map.  In this case, &lt;i&gt;the position of a paragraph in the index has a real meaning in relation to the world.&lt;/i&gt;  If you want to learn about the areas surrounding Calgary, you can literally just look up, down, left, and right of Calgary on the map-index, and you&apos;ll see paragraphs pertaining to the areas that are _actually_ north, south, west, and east of Calgary.
        &lt;/p&gt;
        &lt;p&gt;
          In the same way that scenario (b) is a more powerful way of organizing information than scenario (a), text embeddings are more powerful than keyword indexes.
        &lt;/p&gt;
        &lt;p&gt;
          However, while a 2-dimensional map is intuitive and directly useful to humans, a vector with hundreds of dimensions is not.  Placing information into a highly dimensional space does give the index more structure, and it even has conceptual relevance like locations on a map do, but we aren&apos;t able to make much sense of this space ourselves.  One way to think about using these embeddings for retrieval is to &apos;use the AI model to put our query onto the map&apos; and then &apos;find the documents which are positioned near our query on the map&apos;.
        &lt;/p&gt;
        &lt;p&gt;
          To put that into real terminology, we have to embed our queries using AI models to get a point in space, and then we can use the index to find the nearest neighbors to that point.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:7&quot;&gt;
        &lt;p&gt;
          This is a simplified story.  I neglected to price the storage/database, which is potentially negligible compared to AI inference, and I priced the AI in dollars even though you could run it yourself on your own hardware (at which point you might think only in terms of electricity, if you were going to own the hardware anyway).
        &lt;/p&gt;
        &lt;p&gt;
          &lt;a href=&quot;#fnref:7&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  	&lt;li id=&quot;fn:8&quot;&gt;
        &lt;p&gt;
            An example of this might be to create chunks that contain:
        &lt;/p&gt;
        &lt;p&gt;
            &lt;br /&gt;
            an LLM-generated summary of the previous passages in a given document
            &lt;br /&gt;+&lt;br /&gt;
            the current passage
            &lt;br /&gt;+&lt;br /&gt;
            a summary of the rest of the document.
        &lt;/p&gt;
        &lt;p&gt;
        	&lt;a href=&quot;#fnref:8&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Document Intake and Embedding for Semantic Search -- Part 1</title>
   <link href="https://taf.codes/2025/10/30/reading-intake-and-embed-1.html"/>
   <updated>2025-10-30T00:00:00-04:00</updated>
   <id>https://taf.codes/2025/10/30/reading-intake-and-embed-1</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I build a small service to automatically fetch web documents, push PDFs to my e-reader, extract markdown text content, and store metadata in Mongo.  I will be computing embeddings on these documents to enable semantic-search.&lt;/p&gt;

&lt;h2 id=&quot;what--why&quot;&gt;What?  Why?&lt;/h2&gt;

&lt;p&gt;This first phase actually came from my desire to automate a manual workflow that I used for long articles I wanted to read later.  I’d email them to myself, then load them in Firefox, render a nice printable format with “Reader Mode”, and print PDFs to send to my e-reader.&lt;/p&gt;

&lt;p&gt;Separately, I’d been kicking around ideas for prototypes to use embeddings for semantic-search and retrieval-augmented generation.  These ideas came to a pleasing confluence here.  If I’m already intending to pass a large chunk of my reading through a single point, that’s the perfect opportunity to build intuitions about semantic processing!&lt;/p&gt;

&lt;p&gt;This post is a bit preliminary, it just discusses the document intake pipeline.&lt;/p&gt;

&lt;h2 id=&quot;overview&quot;&gt;Overview&lt;/h2&gt;

&lt;p&gt;Since deciding to build this, I’ve been emailing myself articles and papers using a structured but human-friendly format.  I often find articles on my phone, but I don’t like to read them on there, so I use Android’s “Share” UI to send them via email.  This action costs about 3 taps and takes less than a second.  Sometimes I add comments or tags, which just consists of typing those things and a line prefix (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;##&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Everything goes to an email-alias and automatically ends up in a specific IMAP folder.&lt;/p&gt;

&lt;p&gt;Although this is my primary email account, scripted access is low-stakes because I can use a read-only credential.  The only cost for this safety is that the intake system has to track which messages have been seen already.&lt;/p&gt;

&lt;p&gt;I’d planned to do the IMAP interaction myself, but I was able to skip this by just using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mbsync&lt;/code&gt; to create a local &lt;a href=&quot;https://en.wikipedia.org/wiki/Maildir&quot;&gt;maildir&lt;/a&gt; mirroring the server.  Python’s standard library supports maildirs and RFC-5322 email messages, so that’s all very easy.&lt;/p&gt;

&lt;p&gt;The functionality of Firefox’s “Reader Mode” is powered by &lt;a href=&quot;https://github.com/mozilla/readability&quot;&gt;readability&lt;/a&gt;, so I’ll use this to process pages after I fetch them.&lt;/p&gt;

&lt;p&gt;For maximum compatibility, I wanted to use a real browser to load and render pages.  Playwright is the obvious choice at the moment, and I happened to recall &lt;a href=&quot;https://github.com/simonw/shot-scraper&quot;&gt;shot-scraper&lt;/a&gt; wraps it in a handy CLI.  I &lt;a href=&quot;https://github.com/hephaestus-klytotekhnes/shot-scraper&quot;&gt;created a fork&lt;/a&gt; to add some enhancements to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pdf&lt;/code&gt; command, so I can capture HTML, JSON, PDF, MD, and PNG outputs without loading the page more than once.&lt;/p&gt;

&lt;p&gt;Markdown content and metadata (including, soon, the embeddings) are stored in MongoDB, which I decided to try because they very recently added vector search to the community edition.  PDFs are saved to a Syncthing sync which pushes them to my e-reader.  The PDFs carry a human-memorable ID&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;  in the filename so they can always be easily tied back to the metadata.  This also theoretically permits me to file them (on the e-reader) after I’ve read them, and automatically propagate that human judgement back to the database just by e.g. running an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inotify&lt;/code&gt; watch on the sync dir.&lt;/p&gt;

&lt;h2 id=&quot;code&quot;&gt;Code&lt;/h2&gt;

&lt;p&gt;There’s not much to see yet, but if you’re doing something similar feel free to take a look and borrow whatever: &lt;a href=&quot;https://gitlab.com/t-f/document_intake_and_embedding/-/tree/master/intake?ref_type=heads&quot;&gt;Source on GitLab&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;next-steps&quot;&gt;Next steps&lt;/h2&gt;

&lt;p&gt;The majority of articles are already turning into well-rendered PDFs, but I will be adding some tooling for easy debugging of ugly renders.  Image widths particularly need work.&lt;/p&gt;

&lt;p&gt;As an aside, it would be interesting to learn a bit more about IMAP’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDLE&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NOTIFY&lt;/code&gt; mechanisms.  I had expected originally to be maintaining a persistent connection to my mailserver and getting new messages using something like one of those.  Instead I’m polling, which is totally fine in this case&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;  but isn’t how I thought it would work.&lt;/p&gt;

&lt;p&gt;For the main knowledgebase project, I’ll be experimenting with chunking strategies and embedding models next.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
  	&lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
            I used 5 case-insensitive alphanumeric characters because it&apos;s a good balance between having plenty of IDs and retaining the possibility of carrying one or two in my head.  My thinking is that, e.g. a semantic search query might return these IDs on my computer, but then, to actually access the document, I might prefer to find it on the tablet.  It&apos;s less work to just remember 5 characters than to try and do something elaborate.
        &lt;/p&gt;
        &lt;p&gt;
        	&lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
			`mbsync` isn&apos;t fully naive (it should be downloading only the deltas each time, not fetching all the messages).
        &lt;/p&gt;
        &lt;p&gt;
        	&lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a Foot-Pedal USB Peripheral for Speech-To-Text</title>
   <link href="https://taf.codes/2025/08/10/whisper-pedal.html"/>
   <updated>2025-08-10T00:00:00-04:00</updated>
   <id>https://taf.codes/2025/08/10/whisper-pedal</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;I use a microcontroller to emulate a USB keyboard, stream audio to a transcription model, and then ‘type’ the keystrokes from the resulting text.  A foot-pedal switch is used as the trigger for hands-free usage.&lt;/p&gt;

&lt;h2 id=&quot;what--why&quot;&gt;What?  Why?&lt;/h2&gt;

&lt;p&gt;Working in software, transcription tools have not (historically) been very useful to me. &lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;The open-weights release of OpenAI’s Whisper is an inflection point for my use case.  It’s accurate enough to be, subjectively, worth using.  It can even accept hints for specific vocabulary (such as technical terminology).&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;   Finally, since it’s open-weights, I’m substantially more willing to build workflows around it because I know it’s not about to get yanked out from under me.&lt;/p&gt;

&lt;p&gt;That covers English pretty well, but you’d never want to transcribe &lt;em&gt;code&lt;/em&gt;, right?&lt;/p&gt;

&lt;p&gt;If only there were some sort of way to transform carefully written English into tiny pieces of software…&lt;/p&gt;

&lt;p&gt;Obviously there’s a lot of caveats and limitations involved with using LLMs for software, but I find it’s possible, in some cases, to generate code which (even if incorrect) contains enough correct elements that I can save a meaningful amount of typing.&lt;/p&gt;

&lt;p&gt;Between the LLM-codegen workflow, the fact that I write blog posts sometimes, and the fact that I take more notes than I used to, the time is ripe for speech-to-text in my day-to-day operation.&lt;/p&gt;

&lt;h2 id=&quot;v0-validating-the-concept&quot;&gt;v0: Validating the Concept&lt;/h2&gt;

&lt;p&gt;To get a feel for the real-world value of such a tool, the first thing I did was write a &lt;a href=&quot;https://gitlab.com/-/snippets/4890034&quot;&gt;toy version for the browser&lt;/a&gt;.  This records when I hold the spacebar and generates transcriptions upon releasing it. The tool automatically loads the finished transcription into the clipboard.  That is essentially the best ergonomics I could achieve with extremely low initial investment.&lt;/p&gt;

&lt;p&gt;So the end-to-end flow of an interaction with this tool consists of:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;flicking my mouse over to a browser window, which is very small in the corner of my screen, which causes the window to focus,&lt;/li&gt;
  &lt;li&gt;holding down the spacebar, speaking, and then releasing the spacebar, and then&lt;/li&gt;
  &lt;li&gt;mousing back over to where I need the text and pasting the text to the destination.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/whisper_pedal/v0.gif&quot; alt=&quot;v0 in action&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Although that was already useful enough to save me some keystrokes when composing long passages, I decided that it could be substantially better.&lt;/p&gt;

&lt;p&gt;Particularly, an ideal solution should:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;insert the text at the current cursor position so I don’t lose my place,&lt;/li&gt;
  &lt;li&gt;not require switching between windows,&lt;/li&gt;
  &lt;li&gt;not require removing my hands from the keyboard, and&lt;/li&gt;
  &lt;li&gt;not clobber my clipboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;v1-it-sounds-like-it-should-be-a-keyboard&quot;&gt;v1: It Sounds Like It Should Be a Keyboard&lt;/h2&gt;

&lt;p&gt;After some months of using my v0 tool, I realized that what I really wanted was just “a keyboard, but for my voice”.  Implementation seemed annoying and fiddly, however.  What might work in X might not work in Wayland, and whatever worked in Linux certainly wouldn’t work on Mac or Windows.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Of course, a physical USB keyboard works on Mac, Windows, Linux, and even Android/iOS!  So it sounds like just making a USB-HID device will give me perfect cross-platform functionality.&lt;/p&gt;

&lt;p&gt;It might seem like a slightly strange choice, but, even if it’s a little more difficult, I thought it would be substantially more fun to work on a microcontroller project than on a bunch of platform-specific device input code.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Sticking with the “hands-free” constraint, it seemed fairly obvious that I would either have to use a foot-switch or else something that I could activate by banging my head into it, so I chose a foot-switch.&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;h2 id=&quot;quick-demo&quot;&gt;Quick Demo&lt;/h2&gt;
&lt;div style=&quot;padding:75% 0 0 0;position:relative;&quot;&gt;&lt;iframe src=&quot;https://player.vimeo.com/video/1120679312?badge=0&amp;amp;autopause=0&amp;amp;player_id=0&amp;amp;app_id=58479&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot; title=&quot;Speech Recognition Peripheral with Foot-Switch Interface&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;script src=&quot;https://player.vimeo.com/api/player.js&quot;&gt;&lt;/script&gt;

&lt;h2 id=&quot;development-summary&quot;&gt;Development Summary&lt;/h2&gt;
&lt;p&gt;I settled on using a RP2350 (Raspberry Pi Pico 2 W), because the ESP32-C3 can’t do USB-HID as easily.  An ESP32-S3 would have worked, but why use a proprietary ISA if you don’t have to?&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;#fn:6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;To get going quickly, I started by jumping into CircuitPython.  CircuitPython is great, and you should try it!  Due to a few key limitations, I couldn’t use it to finish this project, but I was very impressed with the development experience.  Even though I didn’t use it for the project, I was happy to have started with it because it let me validate my hardware very easily.&lt;/p&gt;

&lt;p&gt;I ended up using the Pico-SDK with C.  This is a pretty good experience too, but it’s definitely worth a little extra setup time to use a probe and flash via SWD (Single Wire Debug), even if you don’t plan on using the debugger.&lt;/p&gt;

&lt;p&gt;Speech-To-Text is done via OpenAI’s Realtime API, but I am considering switching to local inference with Whisper or one of the derivative projects thereof which more easily supports streaming.&lt;/p&gt;

&lt;h2 id=&quot;future-work&quot;&gt;Future Work&lt;/h2&gt;

&lt;p&gt;I’m interested in running the speech-to-text model locally, and maybe figuring out CUDA context switching.  Ideally, I can find some way to run long background jobs on the GPU constantly, but preempt them to process real-time transcription requests such as would be made by this peripheral.&lt;/p&gt;

&lt;p&gt;Back of the envelope: if you use a RAMdisk, I think it should be possible to load and dump the entire VRAM in under ~1 second.  However, for lack of real preemption, this would just amount to an interjection between other schedulings.  The hypothetical latency of a real-time job therefore depends strongly on the execution duration of the background job’s kernels.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;detailed-build-log--stop-reading-here-but-go-ahead-and-let-your-ai-train-on-it--&quot;&gt;Detailed Build Log &lt;br /&gt;&lt;sup&gt;&lt;sub&gt;&lt;em&gt;( Stop reading here, but go ahead and let your AI train on it. )&lt;/em&gt; &lt;/sub&gt;&lt;/sup&gt;&lt;/h2&gt;

&lt;p&gt;Just in case there’s anything which might be helpful to anyone working on something similar, I decided to share my notes.&lt;/p&gt;

&lt;p&gt;Everything below is just a hasty adaptation of those notes into full sentences with minimal editing.  Unless you have some specific interest, read no further.&lt;/p&gt;

&lt;h3 id=&quot;design&quot;&gt;Design&lt;/h3&gt;
&lt;p&gt;Such a device need only have a fairly narrow set of capabilities. It needs to record audio, send it over Wi-Fi, receive the transcriptions, and then speak USB-HID to a computer.&lt;/p&gt;

&lt;h4 id=&quot;mcu-selection&quot;&gt;MCU Selection&lt;/h4&gt;
&lt;p&gt;I started by considering any microcontrollers with:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;enough RAM for the audio buffer,&lt;/li&gt;
  &lt;li&gt;built-in Wi-Fi, and&lt;/li&gt;
  &lt;li&gt;a well-supported and decently polished toolchain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My mind went first to the ESP32 series, and I was excited to learn that some of these chips come with RISC-V cores and work with the mainline Rust toolchain, which I’m interested to learn.  An ESP32-C3 looked like a good choice initially.&lt;/p&gt;

&lt;p&gt;I was less concerned about USB-HID at first, since I was certain that it’d be easy to delegate USB functionality to another chip.  I remember having an FTDI chip on my old Arduino that could be configured to do this.  Today, you might use something like a &lt;a href=&quot;https://www.adafruit.com/product/5973&quot;&gt;CH9328&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ultimately, more simple is more better though, both for development and for possible manufacture.  A $2 chip isn’t a big deal, but, after all, the ESP32-C3 is only &lt;em&gt;$1&lt;/em&gt;.   When I look at it that way, it starts to seem silly to think about falling back to a fully managed hardware solution.  I’d absolutely do that in a heartbeat if it would save me a hassle, but it looks like both the ESP-IDF and the Raspberry Pi Pico SDK have good built-in support for USB.  So I decided to stick with one vendor, one SDK, and one line on the BOM.&lt;/p&gt;

&lt;p&gt;To do that with an ESP32 series chip, I’d have to bump up from the C3 to the S3 for native USB support.  I am interested to explore some more of Espressif-land, but I would prefer not to use Xtensa and add toolchain complexity.  I’d rather try the Pi Pico 2 W.  The RP2350 looks like a very flexible chip to familiarize myself with.&lt;/p&gt;

&lt;h4 id=&quot;software&quot;&gt;Software&lt;/h4&gt;
&lt;p&gt;Although I had wanted to try a Rust project, CircuitPython is just too easy to pass up.  It looks like I may be able to more or less copy a few examples, string them together, and get a prototype.&lt;/p&gt;

&lt;h4 id=&quot;considering-end-user-configuration&quot;&gt;Considering End-User Configuration&lt;/h4&gt;

&lt;p&gt;I &lt;em&gt;probably&lt;/em&gt; don’t want to mass-produce this, but I was considering giving some prototypes to friends.  The main difference between something good enough for just me and something that could be useful to others is configuration.  I can easily hardcode my Wi-Fi SSID/password and STT API key into the flash, but that won’t work for anyone else.&lt;/p&gt;

&lt;p&gt;CircuitPython is actually great for something like this, because you can mount the device as a USB-MSC device (it pretends to be a thumb-drive, essentially) even while it’s also doing USB-HID and USB-CDC.  Anyone who plugs it in can edit a “settings.toml” file on this virtual flash drive to configure it.&lt;sup id=&quot;fnref:7&quot;&gt;&lt;a href=&quot;#fn:7&quot;&gt;7&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;h4 id=&quot;proof-of-concept-hardware&quot;&gt;Proof-of-Concept Hardware&lt;/h4&gt;
&lt;p&gt;I used a &lt;a href=&quot;https://www.adafruit.com/product/1713&quot;&gt;MAX9814 dev board from Adafruit&lt;/a&gt; to feed the onboard ADC.&lt;/p&gt;

&lt;p&gt;The RP2350’s ADCs produce “12-bit” samples, but they only have 9ish bits of effective resolution.&lt;sup id=&quot;fnref:8&quot;&gt;&lt;a href=&quot;#fn:8&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;   This should be enough for intelligible speech, but the audio quality will be worse than what I’ve been using to evaluate Whisper.  My PC sound-card is probably producing samples with more like 13-15 effective bits of resolution.  It is possible that worse audio quality may impact the accuracy of the transcription model.&lt;/p&gt;

&lt;p&gt;As a fallback option, I also ordered an I2S mic board.  That has its own ADC, provides 24-bit samples, and claims to have 18 bits of effective resolution.  This was relegated to the second-string because it uses an onboard MEMs mic which I probably cannot replace with a wired 3.5mm mono input jack.&lt;/p&gt;

&lt;p&gt;Ultimately, I intend to give this device a standard 3.5mm mic input, so that I can use it with any wired mic.  It seems like proximity may very well be more important than sample resolution.  When testing Whisper through the PC sound-card, I had better results with the mic clipped to my lapel than sitting on the desk, so I expect to do the same with this device.&lt;/p&gt;

&lt;p&gt;A local surplus store had this absolute unit in stock, so that will be the switch.
&lt;img src=&quot;../../../assets/img/whisper_pedal/industrial_foot_switch.jpg&quot; alt=&quot;chonky pedal&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;development-setup&quot;&gt;Development Setup&lt;/h3&gt;

&lt;p&gt;I want to build a tight development loop, and my first concern was that using the device as a USB-HID device would interfere with flashing code onto it.  It would be annoying to have to constantly un-plug, configure, re-plug, flash, un-plug, reconfigure, test.  I started looking into using a second Pi Pico as a &lt;a href=&quot;https://github.com/raspberrypi/debugprobe&quot;&gt;debugprobe&lt;/a&gt;, but that doesn’t turn out to be necessary with CircuitPython.  In my experience, it handled all the USB stuff perfectly!&lt;/p&gt;

&lt;h3 id=&quot;development&quot;&gt;Development&lt;/h3&gt;
&lt;p&gt;I want to build working sketches of the key areas before getting serious about doing anything well.  If I’m correct in my estimation, this stuff should “just work” without too much fuss.  If that assumption is invalid I’d like to find out sooner than later and re-evaluate my priorities.&lt;/p&gt;

&lt;h4 id=&quot;audio-first&quot;&gt;Audio First&lt;/h4&gt;
&lt;p&gt;First I validated that I could get usable audio coming off the RP2350.  This was first because it involves:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;high-ish bandwidth analog stuff,&lt;/li&gt;
  &lt;li&gt;in an interpreted MCU environment,&lt;/li&gt;
  &lt;li&gt;involving a separate amplifier IC I’ve never used, and&lt;/li&gt;
  &lt;li&gt;using a microphone I haven’t tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aside from obvious failure modes (like a defective component or connection), there could be timing issues in the sampling, or maybe the onboard ADC simply isn’t good enough.&lt;/p&gt;

&lt;p&gt;For a quick validation, I wanted to just listen to the audio samples by ear.  I thought about writing to flash and recovering via USB-MSC, but the CircuitPython drive is mounted RO w.r.t the device while it’s attached to a PC.  The easiest thing I could think to do was stream the bytes over Wi-Fi, since that’s supposed to be a first-class experience on the Pi Pico 2 W anyway.&lt;/p&gt;

&lt;p&gt;With minimal tribulations, I was able to record audio using &lt;a href=&quot;https://docs.circuitpython.org/en/stable/shared-bindings/analogbufio/&quot;&gt;analogbufio&lt;/a&gt;, convert it to WAV-compatible samples, and stream them through a socket.&lt;/p&gt;

&lt;p&gt;On the PC side, I just did this.  (TIL about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sox&lt;/code&gt;!)&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;nc &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 4000 &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; out.raw

sox &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; raw &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; 16k &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; signed &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt; 16 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1 out.raw out.wav &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; vlc out.wav&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The audio was passable, but noisy.  This could have something to do with my jumper wires being somewhere near the length of a 2.4GHz halfwave dipole.&lt;/p&gt;

&lt;p&gt;Just to be safe, I took a few recordings in this manner and ran them through Whisper.  Even with the noisy signal, the transcriptions came out fine.  So I’m feeling optimistic, and will jump to the next potential snagging-point.&lt;/p&gt;

&lt;h4 id=&quot;usb-hid-in-circuitpython&quot;&gt;USB-HID in CircuitPython&lt;/h4&gt;

&lt;p&gt;Luckily, emulating a keyboard turns out to be very easy!  I adapted one of the examples given by Adafruit and it worked on the first try.  It’s like 10 lines of code, and you can just pass a string into it to have that string typed out.&lt;/p&gt;

&lt;p&gt;If you want to do this too, be advised that you’ll probably need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;usb_hid&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adafruit_hid&lt;/code&gt;, which are different things.&lt;/p&gt;

&lt;h4 id=&quot;websockets&quot;&gt;Websockets&lt;/h4&gt;
&lt;p&gt;For a realtime experience (and because the device has a limited audio buffer), I’ll be using OpenAI’s streaming transcription API.&lt;sup id=&quot;fnref:9&quot;&gt;&lt;a href=&quot;#fn:9&quot;&gt;9&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;This has an interface based on WebSockets.  CircuitPython, oddly enough, does not include client for WebSockets even though it &lt;em&gt;does&lt;/em&gt; have support for acting as a WebSocket server.  In the worst case, I’m pretty confident I could implement a basic websocket connection myself without much trouble.&lt;/p&gt;

&lt;p&gt;I found &lt;a href=&quot;https://github.com/intGus/cpwebsockets&quot;&gt;cpwebsockets&lt;/a&gt; however, which looks to be a CircuitPython adaptation of a MicroPython library.  This worked well without much effort - looks like I’m lucky again!&lt;/p&gt;

&lt;h4 id=&quot;luck-runs-out&quot;&gt;Luck Runs Out&lt;/h4&gt;

&lt;p&gt;With all the pieces working, I thought I might be able to tie them together.  Unfortunately, this is where I hit the limitations of CircuitPython.&lt;/p&gt;

&lt;p&gt;In CircuitPython, I can use DMA to collect ADC samples in two ways.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;block the CPU and read a specific count of samples, or&lt;/li&gt;
  &lt;li&gt;use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loop=True&lt;/code&gt; to run DMA looping over the same buffer repeatedly, without blocking the CPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the looping method, there’s no way (in CircuitPython) to get a count of how many samples have been written to memory at any given instant.  You can guess based on timing, but that’s pretty ugly, especially in an interpreted environment.&lt;/p&gt;

&lt;p&gt;This limitation with DMA would not be a problem if CircuitPython supported threading/multi-core.  It would be a bit silly to monopolize an entire core just waiting for DMA, but it would work!&lt;/p&gt;

&lt;p&gt;Similarly, if CircuitPython supported handling interrupts, there might have been a way to do this in a single thread.&lt;/p&gt;

&lt;p&gt;Alas though, my heretofore fantastic luck ran out here.&lt;/p&gt;

&lt;h4 id=&quot;what-now&quot;&gt;What Now?&lt;/h4&gt;

&lt;p&gt;I think the answer is just to rewrite this in C.  I expect the Pico SDK will be pretty well polished, and it is definitely well documented.&lt;/p&gt;

&lt;p&gt;The only other possibility that comes immediately to mind would be writing a C helper for CircuitPython.  I have a feeling that could actually be more involved than rewriting it in C though.  CircuitPython is great to use, but I’m not ready to start extending it.&lt;/p&gt;

&lt;p&gt;I’m happy to have validated that the Pi Pico 2 W is plenty powerful enough, that the audio is plausible, and that I can get keystrokes out of it.  I figure that doing things in C may mean that I have a harder time getting the USB-HID stuff to work. In the worst case though, maybe I could reverse engineer the way it’s done in CircuitPython.&lt;/p&gt;

&lt;h3 id=&quot;development-2-c--pico-sdk&quot;&gt;Development 2: C / Pico-SDK&lt;/h3&gt;

&lt;h4 id=&quot;development-setup-2&quot;&gt;Development Setup 2&lt;/h4&gt;
&lt;p&gt;I’m now back to thinking I’ll want a Pico Debugprobe.  I have heard USB can be tricky to get working, and I’d like to simplify matters as much as possible.  One thing that could help is to make sure that the USB stack is &lt;strong&gt;only&lt;/strong&gt; used for HID.  The debugprobe gives me a UART-USB bridge for free, so I’ll use that for output (rather than USB-CDC direct from the RP2350).&lt;/p&gt;

&lt;p&gt;I’d also like to enable flashing over SWD, because that will save me having to do the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unplug -&amp;gt; BOOTSEL -&amp;gt; plug-in -&amp;gt; USB MSC -&amp;gt; mount -&amp;gt; Copy UF2 -&amp;gt; unmount -&amp;gt; unplug -&amp;gt; plug-in&lt;/code&gt; dance.&lt;/p&gt;

&lt;p&gt;I may as well start by setting up the Pico Debugprobe so I can build a workflow around it.&lt;/p&gt;

&lt;h4 id=&quot;pico-debugprobe&quot;&gt;Pico Debugprobe&lt;/h4&gt;
&lt;p&gt;I flashed a spare RP2040 board with the Debugprobe firmware.  (This used to be called Picoprobe, but that conflicted with someone else’s project, so it’s now called Debugprobe.)&lt;/p&gt;

&lt;p&gt;I was able to use the prebuilt &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debugprobe_on_pico.uf2&lt;/code&gt; from &lt;a href=&quot;https://github.com/raspberrypi/debugprobe/releases/tag/debugprobe-v2.2.3&quot;&gt;here&lt;/a&gt;.  Note that there’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debugprobe.uf2&lt;/code&gt; for the officially released hardware (same RP2040 but different board), and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debugprobe_on_pico.uf2&lt;/code&gt; is for running on a Pico board.&lt;/p&gt;

&lt;h4 id=&quot;verify-uart-connectivity&quot;&gt;Verify UART Connectivity&lt;/h4&gt;
&lt;p&gt;While my project board still has CircuitPython, I flashed a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code.py&lt;/code&gt; which just prints “Hello from RP2350!” to UART every 1 second forever.  This gives me a way to verify that my UART bridge works before doing anything else.&lt;/p&gt;

&lt;p&gt;Next, I plugged in the 2040 and wired it up as shown in Figure 10 here:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf#debugprobe-wiring-section&quot;&gt;https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf#debugprobe-wiring-section&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, when I plug the 2040 in with USB and do &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;picocom -b 115200 /dev/ttyACM0&lt;/code&gt;, I see the line “Hello from RP2350!” repeated once per second.&lt;/p&gt;

&lt;h4 id=&quot;setup-pico-sdk-and-flashing-over-swd&quot;&gt;Setup Pico-SDK and Flashing over SWD&lt;/h4&gt;
&lt;p&gt;To check the flashing functionality, I need some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.elf&lt;/code&gt; files.  I will start with the &lt;a href=&quot;https://github.com/raspberrypi/pico-examples&quot;&gt;pico-examples&lt;/a&gt;.  This involves setting up the SDK so I can compile them, and setting up OpenOCD to interface to my RP2350 via my debugprobe.&lt;/p&gt;

&lt;p&gt;The Pico-SDK was pretty easy to set up.  I just added it as submodule and updated the environment in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.envrc&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PICO_SDK_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/pico-sdk&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PICO_PLATFORM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rp2350&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PICO_BOARD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pico2_w&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;To use OpenOCD with a RP2350, you’ll want to use the Raspberry Pi &lt;a href=&quot;https://github.com/raspberrypi/openocd&quot;&gt;fork of OpenOCD&lt;/a&gt;.&lt;sup id=&quot;fnref:10&quot;&gt;&lt;a href=&quot;#fn:10&quot;&gt;10&lt;/a&gt;&lt;/sup&gt;   To compile it, I needed to do this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;./configure &lt;span class=&quot;nt&quot;&gt;--enable-internal-jimtcl&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I’m leery of doing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo make install&lt;/code&gt;, so I chose to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openocd&lt;/code&gt; in the directory where I compiled it.  If you use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-s&lt;/code&gt; flag to specify a search path for TCL scripts, you can run it in-place like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$PATH_TO_RP_OPENOCD_REPO&lt;/span&gt;/src/openocd &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PATH_TO_RP_OPENOCD_REPO&lt;/span&gt;/tcl &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; interface/cmsis-dap.cfg &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; target/rp2350.cfg &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;adapter speed 5000&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;program blink.elf verify reset exit&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That works to flash the board over SWD and connect to it with GDB.&lt;/p&gt;

&lt;h5 id=&quot;project&quot;&gt;Project&lt;/h5&gt;

&lt;p&gt;The repo is here: &lt;a href=&quot;https://gitlab.com/t-f/whisper_pedal&quot;&gt;whisper_pedal&lt;/a&gt;.
&lt;sup&gt;&lt;sup&gt;n.b.: I haven’t written much C in the last decade, so you’d hate to try and learn anything from me!&lt;/sup&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h4 id=&quot;pico-sdk-functionality&quot;&gt;Pico-SDK Functionality&lt;/h4&gt;
&lt;p&gt;At this point, I tested a few more examples from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pico-examples&lt;/code&gt; to confirm that I could:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;read from the ADC,&lt;/li&gt;
  &lt;li&gt;send keystrokes over USB-HID,&lt;/li&gt;
  &lt;li&gt;connect to Wi-Fi, and&lt;/li&gt;
  &lt;li&gt;make an HTTP request,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;all individually.&lt;/p&gt;

&lt;p&gt;Since they work individually, I’m optimistic I can get them all working at once.&lt;/p&gt;

&lt;h4 id=&quot;design-changes&quot;&gt;Design Changes&lt;/h4&gt;

&lt;p&gt;One feature that I’m now missing, having moved away from CircuitPython, is a nice way for would-be users to configure the device.  Ideally it would be possible to provide an SSID and password for the Wi-Fi, as well as potentially providing a custom URL/key to use for the speech-to-text API to facilitate using a different API provider.&lt;/p&gt;

&lt;p&gt;Raspberry Pi’s “Pico Extras” repo contains &lt;a href=&quot;https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/usb_device_msc&quot;&gt;something that implements “USB-MSC”&lt;/a&gt;.  It’s possible that I could use this to make a ‘configuration mode’ that would work as previously designed. I’d basically set aside a small area of the flash to hold the configuration, and make that available via USB-MSC through an emulated filesystem.  That might be a heavy lift though.&lt;/p&gt;

&lt;p&gt;Another thought would be to use a custom UF2 file that updates only a small region of the flash memory that is specially set aside for configuration.  I could provide a web-based utility to generate a UF2 file that can be dragged-and-dropped onto the device (when it’s connected with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BOOTSEL&lt;/code&gt; pressed).&lt;/p&gt;

&lt;p&gt;It might even be possible to use the white-labeling functionality to customize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.htm&lt;/code&gt; which appears on the default device to link to this tool.&lt;/p&gt;

&lt;p&gt;This method could be nice, because it’d save me having to implement my own USB-MSC -&amp;gt; pseudoFS configuration interface or else doing the typical Wi-Fi configuration over an ad-hoc AP setup flow that every IoT device does.&lt;/p&gt;

&lt;h4 id=&quot;software-1&quot;&gt;Software&lt;/h4&gt;

&lt;p&gt;I used core0 to run USB and audio capture, and core1 for Wi-Fi and the API interaction.&lt;/p&gt;

&lt;p&gt;Thread coordination is just a single shared flag (which doesn’t even need to be a semaphore), and 2 queues.&lt;/p&gt;

&lt;p&gt;When audio is being recorded, it’s just pushed onto a queue which is consumed by the networking core.  The audio sub-buffers are sized to fit within the hardware transmission buffer.&lt;/p&gt;

&lt;p&gt;When transcriptions are returned from the API, another queue is used to accumulate a sequence of key-reports.&lt;/p&gt;

&lt;h4 id=&quot;tinyusb&quot;&gt;TinyUSB&lt;/h4&gt;
&lt;p&gt;TinyUSB is indeed, perhaps, a little underdocumented.   Of course, I’m very grateful to have it at all!&lt;/p&gt;

&lt;p&gt;I have adapted some code from the examples, and, since it works, I am going to just take the win and move on.&lt;/p&gt;

&lt;h4 id=&quot;wi-fi&quot;&gt;Wi-Fi&lt;/h4&gt;
&lt;p&gt;Surprisingly, this went very well based on looking at a few examples, a few blog posts, and asking Claude to write it for me.&lt;/p&gt;

&lt;h4 id=&quot;openai-realtime&quot;&gt;OpenAI Realtime&lt;/h4&gt;
&lt;p&gt;The Realtime API is in beta at the time of this writing.  I’m happy with it, but the documentation was a bit unclear and a bit scattered, and/or there may be some missing functionality still.&lt;/p&gt;

&lt;p&gt;In particular, the doc implies that you can use your API key directly to establish a WebSocket connection, and that you can update the properties of said realtime session using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session.update&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transcription_session.update&lt;/code&gt; client events.  In fact, this does not work.  You cannot, for example, turn off VAD (Voice Activity Detection), nor select a specific model.&lt;/p&gt;

&lt;p&gt;However, if you follow the flow which is probably more common in user-facing applications, and you obtain an ephemeral client secret first, you can configure the session in the &lt;em&gt;POST&lt;/em&gt; request at the time you’re getting the client secret.  (This is the same configuration that cannot actually be provided as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session.update&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transcription_session.update&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Even if you do not specifically need a client secret for anything, you may need to request one anyway, just to generate a session to which you can apply configuration.&lt;/p&gt;

&lt;h4 id=&quot;luck-runs-out--again&quot;&gt;Luck Runs Out – Again!&lt;/h4&gt;
&lt;p&gt;While connecting to Wi-Fi and even getting a websocket connection over TLS turned out to be surprisingly easy, using TLS/TCP to stream on the RP2350 turned out to be a problem which is larger than I really feel like dealing with for this project.&lt;/p&gt;

&lt;p&gt;In particular, the altcp send buffer would stop draining after a point, and I struggled for several hours to find out why.  The most likely possibility is that this is my fault for, essentially, not reading enough about what I was doing.  However, there’s always the risk that something is actually broken, undocumented, or just more subtle than expected.&lt;/p&gt;

&lt;p&gt;I was tempted to dive head-on into the interactions between TLS, TCP, and Wi-Fi driver stack, but I had already gone beyond the amount of time I wanted to spend on this project.  Instead I will remain blissfully ignorant of lwip and mbedTLS.  There’s a way to get a quick win here and avoid falling down any rabbitholes.&lt;/p&gt;

&lt;p&gt;I decided to just use UDP.  I already know that TX works, but that there’s a problem which may involve flow control or the TLS integration, and so stripping out all these layers maximizes the chances of nearly immediate success.  This sacrifices some of the elegance of having a fully self-contained peripheral, since I’ll need to write a little bridge from UDP to WSS. That will be something I have to run on my network.&lt;/p&gt;

&lt;p&gt;I have a home-server which is perfect for hosting that utility, but I’m now getting out of the territory of anything I could ever distribute to anyone. &lt;sup id=&quot;fnref:11&quot;&gt;&lt;a href=&quot;#fn:11&quot;&gt;11&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;h5 id=&quot;udp---ws-bridge-server&quot;&gt;UDP &amp;lt;-&amp;gt; WS Bridge Server&lt;/h5&gt;
&lt;p&gt;This is just going to be a simple Python process that listens on a UDP port waiting for 3 types of messages:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;start&lt;/li&gt;
  &lt;li&gt;stop&lt;/li&gt;
  &lt;li&gt;audio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upon &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start&lt;/code&gt;, it’ll open the WSS connection to the server.  Upon &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stop&lt;/code&gt;, it’ll send the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input_audio_buffer.commit&lt;/code&gt; message to the API and trigger transcription.  Audio will just be audio streamed with no redundancy.&lt;/p&gt;

&lt;p&gt;The start and stop messages are not strictly necessary to make this work, but they help trim down the latency and allow shorter interactions with the peripheral to be more ergonomic.&lt;/p&gt;

&lt;h3 id=&quot;finishing-up&quot;&gt;Finishing Up&lt;/h3&gt;

&lt;p&gt;I finished up by replacing the built-in electret mic on the MAX9814 dev board with a couple of leads to a TRRS (3.5mm) breakout; this let me plug in a wired mic so I can wear it on my lapel.&lt;/p&gt;

&lt;p&gt;Remaining to be addressed is some kind of enclosure, and a bit of RF shielding.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
  	&lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
            Circa 2021 Google Docs had some transcription functionality built-in which was sufficiently accurate that it could sometimes be useful for writing English.  Other times, it was frustrating, particularly for more technical writing.  I never got into the habit of using it.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
        &lt;p&gt;
			A perfect example is &quot;Jupyter&quot;, as in JupyterLab or Jupyter Notebook.  Without a hint, the model will give you &quot;Jupiter&quot; every time.  If you provide &quot;Jupyter&quot; as a hint though, it will shift its contextual inference strongly in that direction.  You can still get &quot;Jupiter&quot; with &quot;Jupyter&quot; in the prompt though, if you say something like &quot;the planet Jupiter&quot;.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
        &lt;p&gt;
			To be fair though, nothing works on Windows.  &lt;sub&gt;&lt;sub&gt;OH, GOT &apos;EM!  Microsoft will never recover from this.&lt;/sub&gt;&lt;/sub&gt;
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
        &lt;p&gt;
			I was also considering the possibility that eventually I could share this project.  It would be much more usable for others if it avoided any application dependencies that would have to be installed and run constantly in the background.  Furthermore, any such application would certainly trip everyone&apos;s antivirus software.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
        &lt;li id=&quot;fn:5&quot;&gt;
        &lt;p&gt;
			Of course, I ended up banging my head into it for a good while anyway.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
        &lt;p&gt;
			Sure, sure, ARM is proprietary too.  But the RP2350 has RISC-V onboard, so that&apos;s one step in the right direction.  Anyway, ARM is more widely licensed and seems to have a healthier ecosystem than Xtensa, so I worry a bit less about investing energy into it.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
     &lt;li id=&quot;fn:7&quot;&gt;
        &lt;p&gt;
			Apparently, you can even change the name of the drive without much fuss, so I could call it `VOICE_KEYBOARD` or something other than just `CIRCUITPYTHON`.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:7&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:8&quot;&gt;
        &lt;p&gt;
			The RP2350 ADC can actually sample at 500 kHz.  Depending on how the ADC is losing 3 effective bits, we might find gains by oversampling.  Oversampling by a factor of 4 can, in the exact best circumstance, provide 1 extra effective bit of resolution.  Theoretically, we could oversample a 16 kHz signal by 32x using this ADC, but there are compute and RAM constraints which may make that impossible.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:8&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:9&quot;&gt;
        &lt;p&gt;
			This was in beta as of this writing, and it was a little confusing to find the doc at first, so I&apos;m linking it here in case you&apos;re curious about it.  &lt;a href=&quot;https://platform.openai.com/docs/api-reference/realtime-sessions/transcription_session_object&quot;&gt;OpenAI API Reference&lt;/a&gt;
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:9&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
        &lt;li id=&quot;fn:10&quot;&gt;
        &lt;p&gt;
        	OpenOCD looks like it&apos;s supposed to be very extensible.  I&apos;m not sure why Raspberry Pi needs to maintain a fork, and I haven&apos;t attempted to compare them.  I saw some people using PyOCD and got the impression that it might be more actively maintained (and may be a good choice for RP2350 development), but I&apos;m not certain if it has feature parity with OpenOCD yet.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:10&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:11&quot;&gt;
        &lt;p&gt;
			I thought it would be fun to make something shareable, but, realistically, this is going to be built into most people&apos;s OS&apos;s within several months at the current rate of AI-ification, so there&apos;s not much point.  Macs already have a shortcut where you can double-tap right-command to speak into any text input area -- how long could it possibly take Apple to connect that to a better speech-to-text model?  Probably a third party has already done this.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:11&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>LLMs for Filtering Jobs -- Part 1</title>
   <link href="https://taf.codes/2025/01/07/llms-for-filtering-jobs.html"/>
   <updated>2025-01-07T00:00:00-05:00</updated>
   <id>https://taf.codes/2025/01/07/llms-for-filtering-jobs</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;I annotate some data, and try out Promptfoo to compare some small and small-ish language models.&lt;/p&gt;

&lt;h3 id=&quot;why&quot;&gt;Why?&lt;/h3&gt;
&lt;p&gt;I obtained a list of some job postings, but they are nonspecific.  Only a small portion of them will be relevant to me, and I don’t feel like reading all of them.  Instead I feel like using this as a toy problem for exploring LLM workflows.&lt;/p&gt;

&lt;h3 id=&quot;what&quot;&gt;What?&lt;/h3&gt;

&lt;p&gt;For a first exploratory pass at the problem, my thinking was to simply run subsequent “yes or no” queries.  Instead of bothering to do any prompt engineering, I ran each query through a couple different LLMs and considered the answers where they both agreed (or, at least, did not contradict).  This was surprisingly decent, and would probably work even better if I’d used 3 models and taken the majority decision.  For ad hoc jobs without any particular quality requirement, I think that’s an acceptable starting point.&lt;/p&gt;

&lt;p&gt;However, I rapidly became curious about all of the complexity that I was shoving under the rug with that approach.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Which model was best, given identical prompting?&lt;/li&gt;
  &lt;li&gt;What if I want to scale a pipeline like this? Wouldn’t I rather not burn 2-3x as many tokens as strictly necessary?  Current models are probably trustworthy enough to just use one, for simple queries.&lt;/li&gt;
  &lt;li&gt;Speaking of efficiency, how small of a model could I use?  Can I get anything useful out of a 1 billion parameter model at 2-bit quantization?&lt;/li&gt;
  &lt;li&gt;How complex of a query can I run?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aw heck.  It’s a good day to fall down a rabbit-hole.&lt;/p&gt;

&lt;h3 id=&quot;comparing-small-models&quot;&gt;Comparing Small Models&lt;/h3&gt;

&lt;p&gt;Given a relatively unconsidered prompt, how do different models behave?  Are they accurate for simple queries?  Can we constrain their output to “yes”, “no”, or “insufficient information”?&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;For a simple query, let’s try to ascertain whether a job is remote or not.&lt;/p&gt;

&lt;p&gt;I started by labeling some of the postings using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label-studio&lt;/code&gt;.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;   Then, I just wrote a quick script to transform my labeled JSON data into &lt;a href=&quot;https://www.promptfoo.dev/docs/getting-started/&quot;&gt;Promptfoo&lt;/a&gt; assertions.&lt;/p&gt;

&lt;p&gt;You can see my prompt here, and how several different models and quantizations performed:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/llm_job_filtering/promptfoo_small_models_comparison.png&quot; alt=&quot;Results&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We can see that, with my naive prompting, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpt-4o-mini&lt;/code&gt; does quite well, and then the various 3b and 8b llamas do pretty poorly.  I was curious about the effects of quantization on accuracy in a simple test like this, but it’s too early to tell when they’re all doing this badly.&lt;/p&gt;

&lt;h4 id=&quot;whats-wrong-with-the-llamas&quot;&gt;What’s wrong with the Llamas?&lt;/h4&gt;

&lt;p&gt;Every one except for the smallest adhered pretty consistently to the requested output format.  They just didn’t give the expected responses.&lt;/p&gt;

&lt;p&gt;Part of the problem here may be that my data is a bit dirty – there are posts included in it which are &lt;em&gt;not&lt;/em&gt; job postings.  For labeling purposes, I called these cases “insufficient information”, but they do differ from what I specified I was providing in the prompt.  Maybe gpt-4o-mini is just more graceful when the data differs from the exact expectation?&lt;/p&gt;

&lt;p&gt;I will remove these from the data and we’ll try the eval again.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/llm_job_filtering/promptfoo_small_models_comparison__cleaner_data.png&quot; alt=&quot;Results&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Much better!  It seems like they just couldn’t handle the deviation from their expected input very well.&lt;/p&gt;

&lt;p&gt;I would guess that, with a little prompt engineering to make my exact expectations more explicit, llama3.1:8b could perform similarly in practice to gpt-4o-mini on this task.&lt;/p&gt;

&lt;p&gt;That is potentially significant, since gpt-4o-mini is ~4x the price.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Model&lt;/th&gt;
      &lt;th&gt;Input Cost (per million tokens)&lt;/th&gt;
      &lt;th&gt;Output Cost (per million tokens)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://groq.com/pricing/&quot;&gt;llama3.1:8b&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;$0.05&lt;/td&gt;
      &lt;td&gt;$0.08&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://openai.com/api/pricing/&quot;&gt;gpt-4o-mini&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;$0.15&lt;/td&gt;
      &lt;td&gt;$0.60&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;comparing-mistral-models-under-40gb&quot;&gt;Comparing Mistral models under 40GB&lt;/h4&gt;

&lt;p&gt;I also tried some Mistral models and one derived from a Mistral model.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/llm_job_filtering/promptfoo_mistral_models_comparison__cleaner_data.png&quot; alt=&quot;Results&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It seems like (with the cleaner data) just throwing more parameters at the problem doesn’t do much.&lt;/p&gt;

&lt;h4 id=&quot;will-these-larger-models-handle-the-dirty-data-better&quot;&gt;Will these larger models handle the dirty data better?&lt;/h4&gt;
&lt;p&gt;Just for fun, I will re-run the larger Mistral models against the full set of annotated data.  This includes those posts that are not jobs, for which we (perhaps confusingly) are expecting a response of “insufficient information”.&lt;/p&gt;

&lt;p&gt;My intuition is that the larger models will be slightly more tolerant of this weirdness and more likely to “infer what I actually wanted”.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/llm_job_filtering/promptfoo_mistral_models_comparison__dirty_data.png&quot; alt=&quot;Results&quot; /&gt;&lt;/p&gt;

&lt;p&gt;At a basic anecdotal level, there seems to be some support for this intuition.&lt;/p&gt;

&lt;h4 id=&quot;how-about-a-regex&quot;&gt;How about a regex?&lt;/h4&gt;

&lt;p&gt;I also tried to classify job remoteness using simple string-matching.  This was ~64% accurate over the same (clean) dataset, and saves a few TeraFLOPs per item.&lt;/p&gt;

&lt;h3 id=&quot;using-promptfoo&quot;&gt;Using Promptfoo&lt;/h3&gt;
&lt;p&gt;While writing this post, I tried Promptfoo for the first time.  It provides a convenient UI for this type of comparison between models and between prompts.  It looks like there’s enough flexibility built in to make it do most of what you’d need it to do in this domain.&lt;/p&gt;

&lt;p&gt;I did have one issue with it, though.  Given a list of providers, Promptfoo seems to run tests in-order, running the same test with each provider before continuing to the next test.  This is much, &lt;strong&gt;much&lt;/strong&gt;  slower than running all the tests for each provider serially, &lt;em&gt;if&lt;/em&gt; you’re using a single Ollama server that has to juggle the models in and out of memory.&lt;/p&gt;

&lt;p&gt;Most of the time is spent loading and unloading models, rather than computing anything.&lt;/p&gt;

&lt;p&gt;For a quick workaround, I just commented out all but one model at a time in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promptfooconfig.yaml&lt;/code&gt;, then ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promptfoo eval&lt;/code&gt; once for each model.  Since Promptfoo caches the provider responses, then I could follow up with a final run, with all providers uncommented, in order to generate the nicer, combined report (without actually repeating any provider API calls).&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h3&gt;

&lt;p&gt;I want to find some useful queries which larger models can handle, but smaller ones can’t.  These will also have to be something that I can label reasonably easily.  One obvious low-hanging fruit is to use a model to filter out the posts that aren’t jobs.&lt;/p&gt;

&lt;p&gt;Ultimately, I might like to put my entire resume in-context and see if I can filter for jobs that fit based on that.  It might also be interesting to follow links from the posts to get longer, more detailed job descriptions when they are available, as well as additional information about the companies.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
  	&lt;li id=&quot;fn:1&quot;&gt;
        &lt;p&gt;
            I&apos;ll get into structured output later.  This is just the bare-bones basics.
        &lt;/p&gt;
        &lt;p&gt;&lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;

        &lt;p&gt;
            &lt;a href=&quot;https://labelstud.io/&quot;&gt;Label Studio&lt;/a&gt; is the open-source, core offering of a for-profit company.
        &lt;/p&gt;

        &lt;p&gt;
            It is literally as easy to use as: &lt;code&gt;`pip install label-studio &amp;amp;&amp;amp; label-studio`&lt;/code&gt;, which is pretty neat.  There are built-in templates for labeling different types of data, so it takes about 5 minutes to get a usable labeling UI with keybinds.  Sure, it might have only taken 5 minutes to get Claude to whip up a custom labeling UI, but why do what&apos;s been done?
        &lt;/p&gt;
        &lt;p&gt;
            &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
        &lt;/p&gt;

    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>Dirt-Simple Remote Inference for LLMs</title>
   <link href="https://taf.codes/2024/12/12/dirt-simple-remote-inference-for-llms.html"/>
   <updated>2024-12-12T00:00:00-05:00</updated>
   <id>https://taf.codes/2024/12/12/dirt-simple-remote-inference-for-llms</id>
   <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I’d like an easy way to do batch jobs on remote GPUs.  I don’t want to wait around manually to terminate instances when the jobs are done.&lt;/p&gt;

&lt;h3 id=&quot;what--why&quot;&gt;What?  Why?&lt;/h3&gt;
&lt;p&gt;Like everyone lately, I could sure use some more VRAM.  I don’t have such a volume of ML/AI workloads as warrants investment in local hardware yet.  A100s in the cloud are under 3¢/minute – an order of magnitude cheaper than a long-distance phone call in my birthyear – and RTX 3090s are actually free, if you regard the prices with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%0.2f&lt;/code&gt;, so let’s use ‘em.&lt;/p&gt;

&lt;h4 id=&quot;why-runpod&quot;&gt;Why RunPod?&lt;/h4&gt;

&lt;p&gt;There’s a lot of providers offering GPUs in the cloud at the moment, but RunPod is one of a small number&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;  of them that offers pre-paid &lt;strong&gt;bill-capped&lt;/strong&gt; service.  According to &lt;a href=&quot;https://docs.runpod.io/references/faq?ref=blog.runpod.io#how-does-pod-billing-work&quot;&gt;their FAQ&lt;/a&gt;, they’ll just stop service if you run out of pre-paid credit.&lt;/p&gt;

&lt;p&gt;That’s perfect for doing quick hacky stuff like this, since you can’t accidentally rack up hundreds or thousands of dollars of &lt;a href=&quot;https://news.ycombinator.com/item?id=21697442&quot;&gt;bills&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=28491099&quot;&gt;like&lt;/a&gt; &lt;a href=&quot;https://medium.com/@maciej.pocwierz/how-an-empty-s3-bucket-can-make-your-aws-bill-explode-934a383cb8b1&quot;&gt;many&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=40203399&quot;&gt;people&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=22719573&quot;&gt;have&lt;/a&gt; &lt;a href=&quot;https://chrisshort.net/the-aws-bill-heard-around-the-world/&quot;&gt;done&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=13072830&quot;&gt;on&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=26708148&quot;&gt;AWS&lt;/a&gt; &lt;a href=&quot;https://hackernoon.com/how-we-spent-30k-usd-in-firebase-in-less-than-72-hours-307490bd24d&quot;&gt;and&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=14356409&quot;&gt;GCP&lt;/a&gt;. &lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;   &lt;/p&gt;

&lt;h4 id=&quot;why-not-runpod&quot;&gt;Why NOT RunPod?&lt;/h4&gt;

&lt;p&gt;I’d prefer a full VPS, but RunPod is a container-based service.  This might be preferable if that’s your workflow anyway.  It can however be mildly annoying if you just wanted to leverage a prebuilt image, and naively expect full-featured SSH.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;   &lt;/p&gt;

&lt;p&gt;Their containers do not, by default, get a public IP address.  You can request a public IP, but if you do, it looks to me like you’re pulling from a smaller pool of machines, so you might be more likely to encounter availability issues.&lt;/p&gt;

&lt;p&gt;You might think you don’t need a public IP, but unless your traffic is exclusively HTTP, you probably do.  They have proxying for HTTP, but they won’t forward TCP ports for you &lt;a href=&quot;https://docs.runpod.io/pods/configuration/expose-ports&quot;&gt;unless the host supports a public IP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also encountered at least one issue that feels like a bug.  When calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runpod.create_pod()&lt;/code&gt; with a template, the template’s storage configuration is not respected.  All attempted launches will fail somewhat cryptically unless you directly supply a storage configuration to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create_pod()&lt;/code&gt;.  I was able to find the workaround rather quickly because I happened to think to check their Discord server, but that’s not necessarily the first place one might think to look.&lt;/p&gt;

&lt;p&gt;The vibe I get is that they’re new at this, but improving.  I might hesitate to use RunPod in production, but on balance I like the service.&lt;/p&gt;

&lt;h3 id=&quot;you-said-this-would-be-easy&quot;&gt;You said this would be easy?&lt;/h3&gt;
&lt;p&gt;Yep!  Following the happy-path and just running a simple prebuilt Ollama container with a GPU is pretty easy.&lt;/p&gt;

&lt;p&gt;If you want to use my code below &lt;a href=&quot;https://gist.github.com/hephaestus-klytotekhnes/5e5b729cfaa6544ded6dd71ce0523717&quot;&gt;(also available as a Gist)&lt;/a&gt;, you can just get an API key and use it like:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;runpod_ollama_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tinyllama-test&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA GeForce RTX 3080&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tinyllama&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Tell your favorite joke?&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tinyllama&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;

&lt;p&gt;When using a small model that loads quickly, the service goes live within a couple minutes.  For bigger models, factor in the time to download.  Most of the pods I’ve been given have purported to have pretty decent bandwidth, but then it’s a matter of how much bandwidth the server hosting the models wants to give you.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;   Anyway, for me, for batch use, this is perfectly acceptable.&lt;/p&gt;

&lt;h3 id=&quot;security&quot;&gt;Security&lt;/h3&gt;

&lt;p&gt;There is none.  That HTTP endpoint is publicly accessible with no authentication.&lt;/p&gt;

&lt;p&gt;Ollama doesn’t have anything built in.  Even if it did, it’s a fast-moving project that hasn’t hit v1.0 yet – so, would you trust it?&lt;/p&gt;

&lt;p&gt;Sure, you’re getting a secure tunnel to the proxy, I guess.  Between there and your pod, I think you’re trusting RunPod (and, possibly, a &lt;a href=&quot;https://docs.runpod.io/hosting/overview&quot;&gt;third-party host&lt;/a&gt;!) not to snoop.&lt;/p&gt;

&lt;p&gt;Practically speaking, I’m not very concerned.  My workloads are not sensitive.  For me, I think the most likely (but still unlikely) worst case scenario is that someone somehow guesses one of my pod IDs and piggybacks some free inference.&lt;/p&gt;

&lt;h3 id=&quot;thoughts-for-the-future&quot;&gt;Thoughts for the Future&lt;/h3&gt;

&lt;p&gt;RunPod can also do serverless inference.  They have some semi-pre-configured stuff using vLLM, but it looked slightly more involved than just spinning up an ollama pod.   The pods also have the advantage that I can SSH in and troubleshoot any issues which may come up while I’m getting started.  For now, my workloads are batch-processing things anyway, so a serverless approach doesn’t do me many favors.&lt;/p&gt;

&lt;p&gt;If I have workloads that are less batchy, I may invest some energy in figuring out a serverless solution.  That said, unless you’re paying to keep an instance hot, the latency seems likely to be pretty ugly, and if you do keep an instance hot, it doesn’t sound so serverless after all.&lt;/p&gt;

&lt;h3 id=&quot;the-code&quot;&gt;The Code&lt;/h3&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subprocess&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;contextlib&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;functools&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tqdm&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ollama&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;RUNPOD_API_KEY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;PORT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11434&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LaunchFailureException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Raised when a pod or instance fails to launch properly&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;with_retry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;should_retry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;max_attempts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;backoff&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;
    should_retry is a function that accepts the exception raised by f and
    returns a bool
    &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attempt&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max_attempts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attempt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_attempts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;should_retry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;

            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Attempt &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attempt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; failed. &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
                &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Retrying in &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; seconds...&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;backoff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;delay&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;wait_for_pod_reported_ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ready_timeout_seconds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;450&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Waiting for pod: &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;start_t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ready_timeout_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;runtime&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;runtime&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt;

        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LaunchFailureException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Pod failed to become ready. &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; Pod ID: [&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;pull_with_progress_bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;current_digest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&apos;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Note that streaming mode is necessary, otherwise the request gets timed
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# out, I think because of the proxying through Cloudflare (100 seconds).
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# See doc here:
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;#   https://docs.runpod.io/pods/configuration/expose-ports
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&apos;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_digest&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_digest&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tqdm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tqdm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Pulling &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;19&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;unit_scale&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;completed&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;bar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;progress_bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;completed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;current_digest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load_model_on_server&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load_model_on_server_inner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Loading model...&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;pull_with_progress_bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;with_retry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;load_model_on_server_inner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;should_retry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ollama&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ResponseError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;httpx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RemoteProtocolError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;backoff&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;wait_for_ollama_generate_ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;
    Sometimes /api/generate seems to not be ready immediately after the pull.
    &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;wait_for_ollama_generate_ready_inner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Waiting for /api/generate...&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;check check 123&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;with_retry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;wait_for_ollama_generate_ready_inner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;should_retry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ollama&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ResponseError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;httpx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RemoteProtocolError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;nd&quot;&gt;@contextlib.contextmanager&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;runpod_ollama_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;podspec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA GeForce RTX 3090&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                         &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storage_gb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;128&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; \
            &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ollama/ollama&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;podspec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# use the ids from `runpod.get_gpus()`
&lt;/span&gt;                &lt;span class=&quot;n&quot;&gt;volume_mount_path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/root/.ollama&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/http&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;container_disk_in_gb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storage_gb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# required even with template
&lt;/span&gt;                &lt;span class=&quot;n&quot;&gt;volume_in_gb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storage_gb&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# required even with template
&lt;/span&gt;            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; \
        &lt;span class=&quot;nf&quot;&gt;with_retry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;create_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;should_retry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;QueryError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;does not have the resources to deploy your pod&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt;
                    &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;no longer any instances available&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;wait_for_pod_reported_ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ollama_host_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.proxy.runpod.net&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ollama&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ollama_host_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# You need to load a model before you can get completions.
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Proceeding without loading a model.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Probably this is a mistake.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;load_model_on_server&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;wait_for_ollama_generate_ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Verified /api/generate is ready!&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LaunchFailureException&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lfe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Looks like a bad machine.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lfe&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;stop_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;terminate_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;kill_all_pods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_pods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Found [&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;] pods to kill.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;killed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Stopping pod &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;stop_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Terminating pod &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;runpod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;terminate_pod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;killed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Successfully terminated pod &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Error terminating pod &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pod_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Terminated [&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;killed&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;] pods.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tell_me_a_joke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;runpod_ollama_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tinyllama-test&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA GeForce RTX 3080&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                              &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tinyllama&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Tell your favorite joke?&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tinyllama&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tell_me_a_more_expensive_joke__llama3_3_70b_q8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;llama3.3:70b-instruct-q8_0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;runpod_ollama_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;llama3_3__70b__q8_test&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA A100 80GB PCIe&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                              &lt;span class=&quot;n&quot;&gt;load_model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storage_gb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;
        Tell me your favorite joke, but talk like a pirate.
        To be clear, the joke should not be even remotely nautical.
        No pirate jokes whatsoever.
        Tell me a normal, non-pirate joke.
        Just, you know, tell it like as if you *also* happened to be a pirate.
        &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;__main__&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;tell_me_a_joke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
  	&lt;li id=&quot;fn:1&quot;&gt;
		&lt;p&gt;Vast.ai is the only other one I found, but my search was not exhaustive.&lt;/p&gt;
      	&lt;p&gt;&lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;Some of these examples are more sympathetic than others, but you get the picture.&lt;/p&gt;

&lt;p&gt;Of course, AWS becomes less risky the more time you spend being cautious, but I don&apos;t enjoy caution for caution&apos;s sake.&amp;nbsp;&lt;sup&gt;Anyway, who wants to give money to Jeff when we could give it to literally anyone else?&lt;/sup&gt;&lt;/p&gt;
      &lt;p&gt;&lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
&lt;p&gt;
Some of their prebuilt templates &lt;i&gt;do&lt;/i&gt; use an image that runs SSHd, but not all.  Of the rest, whether or not you can use the trick they suggest &lt;a href=&quot;https://docs.runpod.io/pods/configuration/use-ssh&quot;&gt;here&lt;/a&gt; depends on whether the Docker image involved uses &apos;entrypoint&apos; or &apos;cmd&apos;.  You can override a &apos;cmd&apos;, but they don&apos;t support &lt;a href=&quot;https://docs.docker.com/engine/containers/run/#default-entrypoint&quot;&gt;overriding an &apos;entrypoint&apos;&lt;/a&gt;.
&lt;/p&gt;
&lt;p&gt;
What&apos;s provided by default in all cases is something which looks like SSH access, but isn&apos;t.  They are actually letting you connect to the underlying Docker host, and have set your login-shell to &lt;code&gt;docker exec -it $YOUR_CONTAINER /bin/bash&lt;/code&gt;.  Considering the full context, that&apos;s great, since you can get into any container without having to run your own SSHd.  It does mean however that you are getting &lt;i&gt;only&lt;/i&gt; an interactive console.  There&apos;s no secure tunneling, no SFTP, no SCP, and, in fact, you can&apos;t even do something like &lt;code&gt;ssh $host apt install $package&lt;/code&gt;.
&lt;/p&gt;

&lt;p&gt;
&lt;sub&gt;If you want to force the issue, you could do &lt;code&gt;echo &quot;apt install $package&quot; | ssh -tt $host&lt;/code&gt;, but if you start trying to pipe any non-trivial scripting through the interactive console, it will eventually make you sad.  SSH can&apos;t even forward you an exit code in this case, so you&apos;re getting into &apos;pexpect&apos; territory...&lt;/sub&gt;&lt;/p&gt;

		&lt;p&gt;&lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
	&lt;/li&gt;
	    &lt;li id=&quot;fn:4&quot;&gt;
&lt;p&gt;
Considering my fairly infrequent use, I don&apos;t feel bad about hitting the servers hosting the models.  If you were going to do something similar but more frequently, it might be more responsible to prebuild an image with the model already downloaded, so you&apos;re hitting your container registry rather than the public model servers.
&lt;/p&gt;
&lt;p&gt;
This likely won&apos;t change runtime performance significantly since the Docker host is pulling roughly the same amount of bytes down the same connection, wherever they&apos;re coming from.
&lt;/p&gt;
&lt;p&gt;
		&lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
	&lt;/p&gt;
	&lt;/li&gt;

  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 
 <entry>
   <title>So, you want to weld things?</title>
   <link href="https://taf.codes/2024/07/06/welding-quickstart.html"/>
   <updated>2024-07-06T00:00:00-04:00</updated>
   <id>https://taf.codes/2024/07/06/welding-quickstart</id>
   <content type="html">&lt;p&gt;Everything you need to start welding, below.&lt;/p&gt;

&lt;h3 id=&quot;why&quot;&gt;Why?&lt;/h3&gt;

&lt;p&gt;I decided to learn how to weld.  While a welding machine itself is kind of expensive, I was mildly surprised by how little else you need to get going and make some (basic, but seriously strong) physical structures.&lt;/p&gt;

&lt;p&gt;It seemed to me like the relevant information was spread across a handful of different sources, and this made the very first “pick up a machine and try it” step a little more difficult than it needs to be.  I thought I’d consolidate the information that I felt I needed to begin in one place, in case that might help anyone else who wants to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I am not a welder, and this does not constitute professional advice.  If you place any credence in my words, you will surely burn your house down, electrocute yourself, blind your neighbor’s dog, and many worse things.  You proceed at your own risk.&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;what-process-should-i-start-with&quot;&gt;What process should I start with?&lt;/h3&gt;

&lt;p&gt;I don’t know.  Some people say you’ll “learn more” if you start out with stick welding.  Perhaps they’re right, but I’d just start with whatever is most convenient.&lt;/p&gt;

&lt;p&gt;The list below is not particular to any process.  It covers all of the other things that you might forget and end up accumulating over many false-starts.&lt;/p&gt;

&lt;h3 id=&quot;do-you-have-anything-to-tell-me-about-actually-welding&quot;&gt;Do you have anything to tell me about actually welding?&lt;/h3&gt;
&lt;p&gt;Nope.  You can have my list of what you need to get started though!&lt;/p&gt;

&lt;h3 id=&quot;list-of-things&quot;&gt;List of things:&lt;/h3&gt;
&lt;p&gt;PDF here:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/Welding_beginner_checklist.pdf&quot;&gt;Welding Checklist for Getting Started&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;extra-thoughts&quot;&gt;Extra thoughts:&lt;/h3&gt;
&lt;p&gt;If you’re using a flux process (flux-cored wire or stick welding), you’ll want a slag hammer.&lt;/p&gt;

&lt;p&gt;You can get by using a manual wire-brush for all your brushing needs, but I’m much happier using a right-angle drill with a wire-brush attachment.  A regular drill would work too, but it torques the wrist unpleasantly.&lt;/p&gt;

&lt;p&gt;If you’ve tried all the standard troubleshooting but still can’t get a decent weld, you may want to check the voltage available to your welder.  I learned the hard way that, in some circumstances, a long extension cord can impart a significant-enough voltage drop that it’s nearly impossible to weld properly.&lt;/p&gt;

&lt;p&gt;Learn a thing or two about testing your welds before you trust them in any kind of safety-critical application.  It is possible for a weld to look OK, especially to an untrained eye, but to fail destructively when it’s rolling down the highway at 70mph.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>LLM Teaching-Assistant Prototype Interface</title>
   <link href="https://taf.codes/2024/01/24/llm-tutor.html"/>
   <updated>2024-01-24T00:00:00-05:00</updated>
   <id>https://taf.codes/2024/01/24/LLM-tutor</id>
   <content type="html">&lt;p&gt;I threw together a quick prototype to demonstrate how large multimodal AI models might be used to supplement human TAs in delivering educational courses.&lt;/p&gt;

&lt;p&gt;The idea driving this demonstration was that a relatively simple interface could be used to allow easy questions to be directed to an AI assistant with minimal friction.  In this respect, you could think of the AI model as providing “level 1 tech support”, so the TAs only need to step in for more difficult or nuanced questions.  This could help the same number of TAs more effectively handle a larger number of students.&lt;/p&gt;

&lt;p&gt;Another point to highlight is that, with the right interface, the learning experience can easily come to resemble having a TA looking right over your shoulder every step of the way.  It is not a stretch to imagine that people learn more quickly when they have access to 1-on-1 attention.  An AI assistant may not be as effective as a human TA, but most people cannot afford to have a human TA actually looking over their shoulder throughout the entirety of the learning process.  Persistent, full-time AI accompaniment, however, is quite feasible.&lt;/p&gt;

&lt;h4 id=&quot;proof-of-concept&quot;&gt;Proof of Concept&lt;/h4&gt;

&lt;p&gt;In the video below, you can see a browser window running the prototype interface alongside an instance of Eclipse IDE.  The Eclipse window has been shared into the browser (using the screen-sharing API).  In this way, the AI model is given access to “see” what the user is working on.  Simultaneously, there is a persistent voice connection between the user and the AI model.&lt;/p&gt;

&lt;p&gt;This allows me to talk about what I’m seeing on my screen, and get contextually-aware assistance from the AI.  In the video, I play the first-year computer-science student and we see that the AI is capable of helping me out. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;div style=&quot;padding:128.89% 0 0 0;position:relative;&quot;&gt;&lt;iframe src=&quot;https://player.vimeo.com/video/982237253?badge=0&amp;amp;autopause=0&amp;amp;player_id=0&amp;amp;app_id=58479&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture; clipboard-write&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot; title=&quot;LLM Teaching Assistant Proof of Concept&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;script src=&quot;https://player.vimeo.com/api/player.js&quot;&gt;&lt;/script&gt;

</content>
 </entry>
 
 <entry>
   <title>Minimum Viable Personal Backup Solution</title>
   <link href="https://taf.codes/2023/05/05/backups-mvp.html"/>
   <updated>2023-05-05T00:00:00-04:00</updated>
   <id>https://taf.codes/2023/05/05/backups-mvp</id>
   <content type="html">&lt;p&gt;A simple backup solution for Linux desktops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;
I have a keypress combination to put my desktop to sleep.  I’m replacing that with a script that will run a quick backup and &lt;em&gt;then&lt;/em&gt; sleep.&lt;/p&gt;

&lt;p&gt;The easiest and most robust strategy I could come up with involves &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restic&lt;/code&gt;, some minimal configuration, and Backblaze B2.&lt;/p&gt;

&lt;h3 id=&quot;what--why&quot;&gt;What?  Why?&lt;/h3&gt;

&lt;p&gt;I want backups.  And I want to think about them as little as possible.&lt;/p&gt;

&lt;p&gt;I’ll use BackBlaze B2 for my offsite replication.  This is an object-storage service much like Amazon’s S3.  I use Backblaze because they have good prices and offer decent transparency into their operations.  Over the years, I’ve appreciated their &lt;a href=&quot;https://www.backblaze.com/cloud-storage/resources/hard-drive-test-data&quot;&gt;quarterly reports of hard drive failure rates&lt;/a&gt;, which they publish openly.&lt;/p&gt;

&lt;p&gt;I’ll use Restic because it’s flexible and I know it’s easy to orchestrate from the command line.  I don’t like running services like Duplicati on my computer all the time.&lt;/p&gt;

&lt;p&gt;I want something that runs frequently and runs fast, but which won’t tax my upload bandwidth while I’m actively using my computer.  I already have a keyboard shortcut in my window manager to suspend to RAM.  My thought is to write a script to take a backup and sync to Backblaze, and then simply invoke that automatically every time the computer is about to suspend.&lt;/p&gt;

&lt;p&gt;There’s probably a more graceful way to do this, but I’m aiming for the lowest expenditure of effort here.  Because it’s a desktop, it doesn’t need to sleep immediately like a laptop does.  So, the slightly ugly side of this solution (my computer will be awake for an extra few minutes until the backup completes after I tell it to sleep) is a non-issue.&lt;/p&gt;

&lt;p&gt;This will be a basic 3-2-1 backup strategy.  I want a local backup for ease of use.  In case I nuke something on my computer that I meant to keep, this backup is handy in that circumstance.  Then, to get a remote copy, let’s mirror that backup offsite to Backblaze.&lt;/p&gt;

&lt;p&gt;I want this backup to cover my home directory with 1-day, 1-week, and 1-month copies.&lt;/p&gt;

&lt;p&gt;To implement this scheme in restic, I will have one local repository and one remote repository.  The remote will be a simple mirror.&lt;/p&gt;

&lt;h3 id=&quot;steps-i-took&quot;&gt;Steps I took:&lt;/h3&gt;

&lt;h4 id=&quot;install-restic&quot;&gt;Install restic:&lt;/h4&gt;
&lt;p&gt;As everyone who uses Arch will tell you - I use Arch, so for me installation is:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pacman -S restic&lt;/code&gt;&lt;/p&gt;

&lt;h4 id=&quot;setup-a-backup-repository&quot;&gt;Setup a Backup Repository:&lt;/h4&gt;

&lt;p&gt;I’m going to start out by setting up a restic repo on an external SSD which should be large enough to backup my home folder for quite some time.  (I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;baobab&lt;/code&gt; to check my disk space usage.  It’s my favorite tool for this, as well as for clearing up space.)&lt;/p&gt;

&lt;p&gt;Assuming the external backup drive is mounted at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mnt/bkup_usb_ext&lt;/code&gt;, and you’ve created a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backups/home_slash_$USER&lt;/code&gt; directory there, you can run this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;restic init &lt;span class=&quot;nt&quot;&gt;--repo&lt;/span&gt; /mnt/bkup_usb_ext/backups/home_slash_&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You’ll get a password for that repository that you need to keep track of.&lt;/p&gt;

&lt;p&gt;I wrote a backup script with some excluded directories.  This will take a single
snapshot:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Here, we will take one snapshot.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# You should have already exported the restic password so this can use it.&lt;/span&gt;


&lt;span class=&quot;nv&quot;&gt;LOCAL_RESTIC_REPO&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/mnt/bkup_usb_ext/backups/home_slash_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

restic &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$LOCAL_RESTIC_REPO&lt;/span&gt; backup /home/&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--one-file-system&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--exclude&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;/large_files_you_do_not_want_to_back_up &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--exclude&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;/.cache &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--verbose&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, multiple exclude flags can be used to trim anything out of your backups.  &lt;a href=&quot;https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files&quot;&gt;There’s more documentation here.&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;setup-remote-repo&quot;&gt;Setup Remote Repo:&lt;/h4&gt;

&lt;p&gt;There’s a lot you could unpack here, if you wanted to.  Check out restic’s documentation &lt;a href=&quot;https://restic.readthedocs.io/en/latest/045_working_with_repos.html#ensuring-deduplication-for-copied-snapshots&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make things as simple as possible, I copied the params from the local repo.  For me, the command looked like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;restic &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$REMOTE_RESTIC_REPO&lt;/span&gt; init &lt;span class=&quot;nt&quot;&gt;--from-repo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$LOCAL_RESTIC_REPO&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--copy-chunker-params&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;If you create the remote repo without copying the params from the local repo, you may have issues with deduplication.  Doing it this way seems to ensure that everything “just works”.&lt;/p&gt;

&lt;h4 id=&quot;mirroring&quot;&gt;Mirroring:&lt;/h4&gt;

&lt;p&gt;To mirror, you need to supply both the password of the source repository (local) &lt;em&gt;and&lt;/em&gt; the password for the destination repository (remote).&lt;/p&gt;

&lt;p&gt;You can use environment variables for this, among other things.  Keeping with our mantra of “simplicity”, I’ve used the environment vars.&lt;/p&gt;

&lt;p&gt;Then it’s just a restic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy -–from-repo&lt;/code&gt; command.  (See the whole script below.)&lt;/p&gt;

&lt;h4 id=&quot;retention-policy&quot;&gt;Retention Policy:&lt;/h4&gt;
&lt;p&gt;With restic, the way to implement a retention policy is to take snapshots as desired and then, later, go back and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forget&lt;/code&gt; some of them.  Then, you can &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prune&lt;/code&gt; the repo to remove the unneeded blocks.&lt;/p&gt;

&lt;p&gt;Since I’m taking snapshots at odd times (whenever I put my computer to sleep), I will set up my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forget&lt;/code&gt; command with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--keep-within-...&lt;/code&gt; flags.  Here’s a dry-run of that, which you can run to see which snapshots would be deleted.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$RESTIC_CMD&lt;/span&gt; forget &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-last&lt;/span&gt; 1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-daily&lt;/span&gt; 7d &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-weekly&lt;/span&gt; 1m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-monthly&lt;/span&gt; 6m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-yearly&lt;/span&gt; 2y &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--dry-run&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;checking-structure-checking-data&quot;&gt;Checking Structure, Checking Data:&lt;/h4&gt;

&lt;p&gt;Bitrot is real, and sooner or later a cosmic ray will get the better of every one of us…&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$RESTIC_CMD check –-read-data-subset=1%&lt;/code&gt;&lt;/p&gt;

&lt;h4 id=&quot;simple-alerts-in-case-of-problems&quot;&gt;Simple Alerts in Case of Problems:&lt;/h4&gt;

&lt;p&gt;I will just throw pop-ups on my screen using zenity, with the presumption that I should eventually encounter these.  Ultimately, it would be nice to send myself email but I’m not going to overthink this.&lt;/p&gt;

&lt;h3 id=&quot;the-script&quot;&gt;The Script:&lt;/h3&gt;
&lt;p&gt;If you want to copy and use this, you will have to go line-by-line to fill in your own information.&lt;/p&gt;

&lt;p&gt;Another thing you’ll need, which I did not cover here, is a Backblaze bucket and application key.  There are guides already that can help you set these up.  In the script, you can see some stuff about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AWS&lt;/code&gt;, but this is just because we’re using Backblaze’s B2 as an S3 clone.  They provide an API compatible with S3 clients.  Restic’s S3 support is better than its B2 support, and Backblaze’s S3-compatibility is solid, so I think this is the way to go.&lt;/p&gt;

&lt;p&gt;Obviously this is provided without warranty, but feel free to use and modify any way you like.  If you share it publicly, please cite this page.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#I want this script to abort on the first sign of failure and give me a pop-up in case it does&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;trap &lt;/span&gt;alert ERR

alert&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	zenity &lt;span class=&quot;nt&quot;&gt;--warning&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--text&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Check your restic log at /tmp/restic.log&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--title&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;BACKUP ERROR!&quot;&lt;/span&gt; &amp;amp;
	&lt;span class=&quot;nv&quot;&gt;zenity_pid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$!&lt;/span&gt;

    	&lt;span class=&quot;c&quot;&gt;# Wait a little for the Zenity window to appear&lt;/span&gt;
    	&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;1
    	&lt;span class=&quot;c&quot;&gt;#In i3, this will focus the window.  You will want to remove this if you don&apos;t use i3.&lt;/span&gt;
    	i3-msg &lt;span class=&quot;s1&quot;&gt;&apos;[class=&quot;^Zenity$&quot;] focus&apos;&lt;/span&gt;

    	&lt;span class=&quot;c&quot;&gt;# Make sure to wait for the Zenity dialog to be closed before exiting the script,&lt;/span&gt;
   	 	&lt;span class=&quot;c&quot;&gt;# or else the script might exit before anyone ever sees the dialog&lt;/span&gt;
    	&lt;span class=&quot;nb&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$zenity_pid&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Initialize Local Environment&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;LOCAL_RESTIC_REPO&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/mnt/bkup_usb_ext/backups/home_slash_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RESTIC_PASSWORD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;this would be the password to your REMOTE repo&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RESTIC_FROM_PASSWORD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;this would be the password to your LOCAL repo&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Initialize Environment for Remote Repository:&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;S3_COMPAT_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3.us-west-000.backblazeb2.com&quot;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# This should also be updated!&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;BUCKET_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;YOUR_BUCKET_NAME_HERE&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;BUCKET_DIR_PREFIX&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;restic&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;TF_B2_BKUP_APPLICATION_KEY_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;YOUR_BACKBLAZE_KEY_ID&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;TF_B2_BKUP_APPLICATION_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;YOUR_BACKBLAZE_KEY&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# These two need to be exported as if they&apos;re AWS-related, because we&apos;re using the S3 driver of restic&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# with BackBlaze&apos;s S3 compat layer&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TF_B2_BKUP_APPLICATION_KEY_ID&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TF_B2_BKUP_APPLICATION_KEY&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;RESTIC_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;restic -r &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$LOCAL_RESTIC_REPO&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REMOTE_RESTIC_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;restic --repo s3:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;S3_COMPAT_ENDPOINT&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;BUCKET_NAME&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;BUCKET_DIR_PREFIX&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Clear logfile from last run&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mv&lt;/span&gt; /tmp/restic.log /tmp/restic.log.1 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### We&apos;re starting the backup now: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ###&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1


&lt;span class=&quot;c&quot;&gt;# Do a snapshot into the local repo&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$RESTIC_CMD&lt;/span&gt; backup /home/&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--one-file-system&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--exclude&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;/large_files_you_do_not_want_to_back_up &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--exclude&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;/.cache &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--verbose&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1


&lt;span class=&quot;c&quot;&gt;# Local Cleanup.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# This gets rid of the extraneous snapshots we no longer need to store per my rules&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### Forget and Prune ###&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nv&quot;&gt;$RESTIC_CMD&lt;/span&gt; forget &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-last&lt;/span&gt; 2 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-daily&lt;/span&gt; 7d &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-weekly&lt;/span&gt; 1m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-monthly&lt;/span&gt; 6m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--keep-within-yearly&lt;/span&gt; 2y &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;--prune&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1


&lt;span class=&quot;c&quot;&gt;# Check structure + check actual data&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# If you do backups less frequently, or are more concerned about integrity, you&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# might increase that percentage.  As written, this checks 1% of the backup.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### Check and Read Data ###&quot;&lt;/span&gt; 	&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nv&quot;&gt;$RESTIC_CMD&lt;/span&gt; check &lt;span class=&quot;nt&quot;&gt;--read-data-subset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1% &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1


&lt;span class=&quot;c&quot;&gt;###&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Remote Repo Stuff&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;###&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;#########&quot;&lt;/span&gt; 			&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### BEGIN REMOTE REPO BUSINESS ###&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;#########&quot;&lt;/span&gt; 			&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### Mirror local to remote ###&quot;&lt;/span&gt; 	&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nv&quot;&gt;$REMOTE_RESTIC_CMD&lt;/span&gt; copy &lt;span class=&quot;nt&quot;&gt;--from-repo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$LOCAL_RESTIC_REPO&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1

&lt;span class=&quot;c&quot;&gt;# Clean up extraneous snapshots and free up the space used by removed blocks&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### Forget and Prune ###&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nv&quot;&gt;$REMOTE_RESTIC_CMD&lt;/span&gt; forget &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--keep-last&lt;/span&gt; 2 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--keep-within-daily&lt;/span&gt; 7d &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--keep-within-weekly&lt;/span&gt; 1m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--keep-within-monthly&lt;/span&gt; 6m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--keep-within-yearly&lt;/span&gt; 2y &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--prune&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1


&lt;span class=&quot;c&quot;&gt;# Check structure + check actual data&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;### Check and Read Data ###&quot;&lt;/span&gt; 	&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1
&lt;span class=&quot;nv&quot;&gt;$REMOTE_RESTIC_CMD&lt;/span&gt; check &lt;span class=&quot;nt&quot;&gt;--read-data-subset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1% &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/restic.log 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;!--&lt;script src=&quot;https://gitlab.com/-/snippets/3728467.js&quot;&gt;&lt;/script&gt;--&gt;
</content>
 </entry>
 
 <entry>
   <title>Dirt-Simple Ergonomic Reading Chair</title>
   <link href="https://taf.codes/2021/10/04/ergonomic-reading-chair.html"/>
   <updated>2021-10-04T00:00:00-04:00</updated>
   <id>https://taf.codes/2021/10/04/ergonomic-reading-chair</id>
   <content type="html">&lt;p&gt;I wanted a comfortable way to read for extended periods, and this is what I came up with.&lt;/p&gt;

&lt;h5 id=&quot;the-short-of-it&quot;&gt;The Short of It&lt;/h5&gt;

&lt;p&gt;&lt;a href=&quot;https://www.ikea.com/us/en/cat/poaeng-series-07472/&quot;&gt;IKEA Poang&lt;/a&gt;&lt;br /&gt;
+&lt;br /&gt;
&lt;a href=&quot;https://a.co/d/0fHAwO0W&quot;&gt;Wali Monitor Arm&lt;/a&gt;&lt;br /&gt;
+&lt;br /&gt;
&lt;a href=&quot;https://a.co/d/0cdUA6FC&quot;&gt;Basic reading stand&lt;/a&gt;&lt;br /&gt;
=&lt;br /&gt;
“pretty good reading chair”&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The bamboo is prone to splitting, so I used very small screws to secure it to the VESA mount, but it’s held up well.&lt;/p&gt;

&lt;p&gt;This setup goes especially well with a large e-reader, as you don’t have to unclip it from the stand to flip the pages.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/reading_chair.jpg&quot; alt=&quot;Reading Chair&quot; /&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Reverse-Engineering Philips Wiz Smart Bulbs</title>
   <link href="https://taf.codes/2019/10/13/reverse-engineering-philips-wiz-bulbs.html"/>
   <updated>2019-10-13T00:00:00-04:00</updated>
   <id>https://taf.codes/2019/10/13/reverse-engineering-philips-wiz-bulbs</id>
   <content type="html">&lt;p&gt;I reverse-engineered my Wiz smart-bulbs so I could control them without the Wiz app.&lt;/p&gt;

&lt;p&gt;I’m going to walk through my reverse-engineering process here, just in case that helps anyone.  If you mainly want to know about the protocol, I’m sure someone else has already documented it by now.  If you want a library to control them without much fuss, you may be out of luck for the time being.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt;&lt;br /&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;params&quot;:{&quot;b&quot;:255,&quot;c&quot;:0,&quot;g&quot;:0,&quot;r&quot;:15,&quot;w&quot;:43},&quot;id&quot;:542,&quot;method&quot;:&quot;setPilot&quot;}&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/udp/10.0.0.95/38899&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You can guess what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; are.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;w&lt;/code&gt; are for “cool white” and “warm white” respectively.&lt;/p&gt;

&lt;p&gt;Did you know you can send UDP packets directly from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash&lt;/code&gt;?&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;   Me neither.&lt;/p&gt;

&lt;h3 id=&quot;why&quot;&gt;Why?&lt;/h3&gt;

&lt;p&gt;These bulbs themselves are pretty decent, but I don’t like the Wiz app.  Partly for privacy reasons, and partly because I don’t want to rely on a proprietary application to control something basic and fundamental like my lights.&lt;/p&gt;

&lt;p&gt;I was also considering building a fun little physical controller for each room’s lights with e.g. an ESP32 and 5x linear (sliding) potentiometers.  Nothing beats a correctly located physical control.  As an apartment-dweller, I like to have the dimming built into the bulbs themselves, but admittedly they really were onto something way back when they started installing lightswitches at the entrances and exits to rooms.&lt;/p&gt;

&lt;p&gt;Also, if we’re being honest, I just wanted to fool around with Wireshark and learn how it works.  I’ve had half a mind for a while now to start monitoring other devices in my home and see what kind of information they leak.  This was a good first step, since, like everyone’s first Arduino project, it has the satisfying culmination: “Hey look, I can make the lights blink!”&lt;/p&gt;

&lt;h3 id=&quot;background&quot;&gt;Background&lt;/h3&gt;

&lt;p&gt;The bulbs I have are the Philips &lt;em&gt;Wiz&lt;/em&gt; Bulbs, which are controlled primarily via WiFi.  These are distinct from the Philips Hue bulbs which require a base-station and (IIRC) are using something like 433 or 900 MHz for control input.&lt;/p&gt;

&lt;p&gt;They’ve got something like an ESP8266 inside, and can host their own WiFi AP for setup.  Essentially all that this does is allow you to push the credentials for your main home WiFi network onto the bulb using the Wiz app.  Once the bulb has saved the credentials for your home network, all further interactions will occur there.&lt;/p&gt;

&lt;p&gt;It’s reasonable to presume that they’re authenticating and securing a DHCP lease.  For the sake of convenience, I might have my router’s DHCP server assign specific IP addresses to them, so they won’t hop around as the leases expire.&lt;/p&gt;

&lt;p&gt;After they’re connected, they could be listening on some port, or they might theoretically be polling some Wiz server, but the latter seems unlikely.&lt;/p&gt;

&lt;p&gt;I expect that I can sniff the wireless traffic, record all the packets going to the bulb or coming from it, and then analyze those to reverse-engineer the protocol.  Theoretically, there could be encryption set up in such a way as to require reverse engineering the Wiz app itself to defeat it, but I doubt they’d have bothered.&lt;/p&gt;

&lt;h3 id=&quot;setting-up-wireshark&quot;&gt;Setting Up Wireshark&lt;/h3&gt;

&lt;p&gt;I’ve hooked up an old, cheap wireless dongle which supposedly has decent Linux drivers.&lt;/p&gt;

&lt;p&gt;However, Wireshark starts up without any interfaces available.  I definitely remember, once, being able to select an interface to monitor on this screen.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/philips-wiz/1-wireshark.png&quot; alt=&quot;Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You have to add your user to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wireshark&lt;/code&gt; group, and then you probably have to log out and log back in again to get that group attached to your login shell.  I will instead live dangerously and opt for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo wireshark&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/philips-wiz/2-wireshark.png&quot; alt=&quot;Screenshot&quot; /&gt;
It works perfectly now.&lt;/p&gt;

&lt;h3 id=&quot;monitoring-the-wireless-network&quot;&gt;Monitoring the Wireless Network&lt;/h3&gt;

&lt;p&gt;If you simply double-click on a wireless interface that’s not already associated to an AP, you’ll get no packets:
&lt;img src=&quot;../../../assets/img/philips-wiz/3-wireshark.png&quot; alt=&quot;Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;When I checked &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ip link&lt;/code&gt;, the interface is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state: DOWN&lt;/code&gt;.  To get the interface up and associated, you can use tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wifi-radar&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wicd&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NetworkManager&lt;/code&gt;, etc.  You can even use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wpa_supplicant&lt;/code&gt; manually, but it’s a pain.  The easiest thing you could possibly do is move to a machine that’s already associated with the Wireless AP you want to monitor, so that’s what I did.&lt;/p&gt;

&lt;p&gt;If this works, you’ll see a great many packets.  Who knew your computer did this much talking behind your back?  Not to worry, I’m sure they’re all fine.
&lt;img src=&quot;../../../assets/img/philips-wiz/many_packets.png&quot; alt=&quot;Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We’ll need some way to filter these packets.  The IP or the MAC address for one of the lightbulbs would be the natural choice.  I logged into my router to see an overview of the devices it was aware of, and it helpfully provides MAC addresses for everything.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/philips-wiz/comcast.png&quot; alt=&quot;Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Wiz also helped us out a little - the hostnames of the Wiz bulbs all start with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wiz_&lt;/code&gt;.  If you’re reverse-engineering something less helpful, it may be worth knowing that you can sometimes look up the manufacturer of a network device based on the MAC address.&lt;/p&gt;

&lt;p&gt;In Wireshark, you can use a filter like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eth.addr == xx:xx...:xx&lt;/code&gt; for MAC addresses.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../../assets/img/philips-wiz/5-wireshark.png&quot; alt=&quot;Screenshot&quot; /&gt;
If you look at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Source&lt;/code&gt; column for these Ethernet frames, you can see that Wireshark has replaced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A8:BB:50&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WizIot&lt;/code&gt;.  This is because it could look up the manufacturer for that MAC address prefix.  Neat.&lt;/p&gt;

&lt;p&gt;Those packets above were collected by turning the bulbs off and then back on.  You can see they request IP addresses from DHCP.  One of them was granted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10.0.0.242&lt;/code&gt;.  Next, I’ll filter based on that IP address instead of the MAC.  This will still filter for that specific lightbulb, but it will also naturally exclude all traffic below IP.  The lower-level stuff is just noise to me at this point.&lt;/p&gt;

&lt;h3 id=&quot;to-be-continued&quot;&gt;To be continued&lt;/h3&gt;
&lt;p&gt;After the above, I had to pivot and capture the packets a slightly different way.  I will do a write-up of this process later.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;hr /&gt;
  &lt;ol&gt;
  	&lt;br /&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
It&apos;s worth noting that, even though it &lt;i&gt;appears&lt;/i&gt; from the command like there might be a virtual device in &lt;code&gt;/dev/&lt;/code&gt;, there&apos;s not.  Obtusely, this is actually a special bash feature.
      &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;↩&lt;/a&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
 </entry>
 

</feed>
