<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like infinite-avatars, thinking-more-about-backups but not debugging-beyond-the-debugger, docker-automatic-nginx-proxy, migrating-from-github-to-forgejo, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/infinite-avatars,thinking-more-about-backups/unlike/debugging-beyond-the-debugger,docker-automatic-nginx-proxy,migrating-from-github-to-forgejo,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2025-11-01T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Thinking more about backups</title>
        <link href="https://chameth.com/thinking-more-about-backups/"/>
        <updated>2025-11-01T00:00:00Z</updated>
        <id>https://chameth.com/thinking-more-about-backups/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/thinking-more-about-backups/backblaze.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/thinking-more-about-backups/backblaze.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/thinking-more-about-backups/backblaze.png&#34; alt=&#34;The Backblaze logo: a stylised flame above the word Backblaze&#34; loading=&#34;lazy&#34; width=&#34;500&#34; height=&#34;320&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The Backblaze logo&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Almost a year ago I wrote about &lt;a href=&#34;https://chameth.com/simple-backups-restic-hetzner/&#34;&gt;how I do backups with Restic and Hetzner&lt;/a&gt;.
That system has been ticking along well ever since, but recently I had some… thoughts. These backups are
all well and good if I accidentally delete a file, or a database gets corrupted, or something, but there are
two glaring issues:&lt;/p&gt;
&lt;p&gt;Firstly, I’m backing up my Hetzner server to Hetzner cloud storage. If something happens to Hetzner — or
my Hetzner account — then all my eggs go down with that basket. Obviously Hetzner are a big organisation
and aren’t likely to just vanish overnight, but I’m less confident about my account. Could a false abuse
report get it suspended? What if the UK passes
&lt;a href=&#34;https://www.legislation.gov.uk/ukpga/2023/50/contents&#34;&gt;even more dumb laws&lt;/a&gt; and Hetzner decide it’s easier
just to not do business with people here? This is the same sort of concern I have about Google accounts:
if you have half of your life in Google Drive and Google Mail, what happens if you comment on a YouTube
video, get flagged by an AI moderation process, and your account gets suspended? It’s probably not very
likely, but these are things my brain likes to dwell on.&lt;/p&gt;
&lt;p&gt;Secondly, the credentials to access the backups sit on each machine that is backed up. If someone malicious
gained access to the machine, they’d also have access to delete or tamper with all the backups. It feels
a little silly that the same attack could take down both the originals and the backups. There’s no way to
avoid that with Hetzner’s S3 implementation, as far as I can tell.&lt;/p&gt;
&lt;h3 id=&#34;exploring-options&#34;&gt;Exploring options&lt;/h3&gt;
&lt;p&gt;I toyed with the idea of making local copies of the backups, but the only way to avoid the same problems
would be to keep them offline and do a manual copy every now and then. I didn’t really want to do that,
and was concerned that if I did a monthly offline backup then I stood to lose up to a month of data in
the worst case.&lt;/p&gt;
&lt;p&gt;I then looked around at other S3 providers. &lt;a href=&#34;https://aws.amazon.com/s3/storage-classes/glacier/&#34;&gt;Amazon’s glacier offering&lt;/a&gt;
is tempting due to its very low storage costs, but you pay for that if you ever want to restore anything.
There are also lots of weird pricing edge cases around moving data between storage classes, minimum file
sizes, and so on. A much better option is &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34;&gt;Backblaze’s B2&lt;/a&gt; product.
Their pricing is much more straight-forward, and they have an interesting feature that’s particularly useful
in this case: &lt;a href=&#34;https://www.backblaze.com/blog/backblaze-b2-lifecycle-rules/&#34;&gt;lifecycle rules&lt;/a&gt;. Coupled with
the ability to create API keys that don’t have access to delete files (just “hide” them), this allows for
what’s effectively an append-only store.&lt;/p&gt;
&lt;p&gt;This works more-or-less out of the box with Restic. &lt;a href=&#34;https://pricey.uk/blog/restic-backups-without-delete/&#34;&gt;Joseph Price has a guide&lt;/a&gt;
that goes into the setup in a bit more depth. Basically, whenever Restic would delete a file (e.g. during
a “forget” or “prune” operation), it instead gets hidden and is only deleted when the B2 lifecycle rules
decide it should be. I’ve kept the existing Hetzner S3 backups for now, and just added an extra step to
the end of my script: a simple &lt;code&gt;restic copy&lt;/code&gt; and a &lt;code&gt;restic forget&lt;/code&gt;. B2 actually works out cheaper than the
Hetzner storage, as they don’t bill you for a minimum of 1TB storage; my current usage is around $3/month.
Not a bad price for some extra peace of mind!&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Making a font of my handwriting</title>
        <link href="https://chameth.com/making-a-font-of-my-handwriting/"/>
        <updated>2025-08-08T00:00:00Z</updated>
        <id>https://chameth.com/making-a-font-of-my-handwriting/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Recently I’ve been on a small campaign to try to make my personal website
more… personal. Little ways to make it obvious it’s &lt;em&gt;mine&lt;/em&gt; and &lt;em&gt;personal&lt;/em&gt;,
not just another piece of the boring corporate dystopia that is most of
the web these days. I don’t quite want to fully regress to the Geocities era
and fill the screen with animated under construction GIFs, but I do want to
capture some of that vibe.&lt;/p&gt;
&lt;p&gt;I’d added some bits and pieces along those lines: floating images in articles
now look like they’re stuck to the page with sellotape, related post
links have a wavy border that animates when you hover over them, and so on.
Next, I wanted to change the heading fonts from a monospace font to something
cursive, to resemble handwriting. Less terminal output, more handwritten letter.
I couldn’t find one I liked, though. So why not make my own? It can’t be that
hard, right?&lt;/p&gt;
&lt;h3 id=&#34;failing-to-do-it-myself&#34;&gt;Failing to do it myself&lt;/h3&gt;
&lt;p&gt;I set out to try to make the font myself using open source tools. After doing
a bit of research, it seemed like the general approach was to create vectors of
each character and then import them into a font editor. That seems to mean
either Adobe Illustrator and FontLab (if you have too much money) or Inkscape
and FontForge (if you like open source). I fall firmly into the latter category,
so I grabbed my graphics tablet and opened Inkscape.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;I wrote out my first three letters: capital A, B and C. Saved them in Inkscape,
and attempted to import them into FontForge. Then I remembered one crucial
thing that had slipped my mind: I absolutely loathe using FontForge. It’s a bit
like when you open an old version of GIMP and get a bunch of weird looking
windows floating all over the place; it feels like you’re fighting against the
tool to do even the most basic operations. The difference is I have cause to
edit images a &lt;em&gt;lot&lt;/em&gt; more than I edit fonts, and GIMP has actually significantly
improved their UI over the years.&lt;/p&gt;
&lt;p&gt;Here are the rough steps I went through with FontForge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Launch Font Forge. It shows a weird bit of art in one window, and an open
file dialog in another.&lt;/li&gt;
&lt;li&gt;I don’t want to open a file, so I close that dialog. The program exits.&lt;/li&gt;
&lt;li&gt;Relaunch Font Forge, and realise that within the “Open Font” dialog is a
“New” button. Click it.&lt;/li&gt;
&lt;li&gt;Get to the standard font-editing UI. Right-click on the “A” looking for
a way to import an SVG. Don’t see one.&lt;/li&gt;
&lt;li&gt;Click around a bit, exploring the menus. Everything feels a bit off.
You can’t open one menu then hover over the next to see its content, like
basically every UI toolkit in existence. I think FontForge has eschewed QT
and GTK in favour of doing things itself.&lt;/li&gt;
&lt;li&gt;Find the “Import” option in the File menu. Hope it’s for a single glyph not
the whole font.&lt;/li&gt;
&lt;li&gt;A file picker opens. Again it’s all a bit off from normal desktop
conventions. Try to resize it, and just get blank grey space at the bottom.&lt;/li&gt;
&lt;li&gt;Type the absolute path I want to go to in the text field.&lt;/li&gt;
&lt;li&gt;Get a dialog saying “Not a bdf file /home/chris/etc”. Press OK.&lt;/li&gt;
&lt;li&gt;Get a dialog saying “Could not find a bitmap font in”. Press OK.&lt;/li&gt;
&lt;li&gt;Press Ctrl+L to see if that lets me enter a path. Click everything in the
dialog to try to find a way to enter a path. Get annoyed. Give up. Click
through folder-by-folder to get to where I want to be.&lt;/li&gt;
&lt;li&gt;Get to the folder and don’t see any files. Change the format to “SVG”.
Double-click the newly-visible SVG file.&lt;/li&gt;
&lt;li&gt;Get a dialog saying “You must select a glyph before you can import an image
into it”. Press OK.&lt;/li&gt;
&lt;li&gt;The import dialog goes away, having not imported.&lt;/li&gt;
&lt;li&gt;Select the glyph in the main tool area, then repeat the File→Import dance.&lt;/li&gt;
&lt;li&gt;It’s actually there now! Open the glyph in the editor and see it’s
a complete mess of Bézier curves. I can’t click what I want without
accidentally moving a handle for an adjacent curve.&lt;/li&gt;
&lt;li&gt;Rage-quit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’m sure FontForge is less anger inducing once you’re used to it. And you
definitely could use it to build a font like this if you had much more patience
than me. I’d had enough of death-by-a-thousand-paper-cuts though.&lt;/p&gt;
&lt;p&gt;I briefly tried Inkscape’s built-in support for making an SVG font. It annoyed
me a lot less, but it’s fiddly: it seemed like each glyph had to be a single
path, so you had to convert the glyphs to paths, then merge them correctly.
If you merge them incorrectly then the wrong bits of your letters end up
filled (like the inside of the ‘B’). Path manipulation is getting towards the
limit of my knowledge of vector editing, and it took a bit of trial and error
for each letter that had more than a single stroke. I didn’t fancy doing that
for every letter.&lt;/p&gt;
&lt;p&gt;I’m usually a big advocate of open source, but this was one of those painful
times when it feels like it just falls short. Clunky, painful UI and processes
where commercial tools just let you get on with your work.&lt;/p&gt;
&lt;h3 id=&#34;you-can-exchange-money-for-goods-and-services&#34;&gt;You can exchange money for goods and services&lt;/h3&gt;
&lt;p&gt;When I’d been looking for open source tutorials, I found many mentions of
a closed source, hosted tool: &lt;a href=&#34;https://www.calligraphr.com/en/&#34;&gt;Calligraphr&lt;/a&gt;.
It has a free version with limitations (no ligatures, no variations, 75
glyphs per font), and a pro version for £8/month. I’d normally balk at the
idea of a subscription for this, but they have the perfect answer: you can
make a one-time payment, and your account automatically downgrades back to free
after a month. It’s not a hidden option, either, it’s the most prominent button
on the upgrade page. That made me happy to give them £8 to play around with
the service for a month.&lt;/p&gt;
&lt;p&gt;Calligraphr works by having you print templates, write out the letters, then
scan them in. It does some magical processing to extract the glyphs, provides
tools to tidy them up, align them, etc, and then produces a TTF file for you.
You can see some of my completed templates here:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/making-a-font-of-my-handwriting/template.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/making-a-font-of-my-handwriting/template.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/making-a-font-of-my-handwriting/template.jpg&#34; alt=&#34;Eight scanned template sheets, filled in with handwritten letters and ligatures&#34; loading=&#34;lazy&#34; width=&#34;1166&#34; height=&#34;841&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Most of the templates I used for the font&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Calligraphr has a nice UI to generate the templates, allowing you to select
which glyphs to include. I added the “minimal English”, “basic punctuation”
and “Ligatures” sets. That gave me four pages to fill out, and I did them all
twice. That let me filter out versions that didn’t work well, and have
variants for some letters so the font wasn’t too repetitive. Later on, I went
back and added some custom ligatures based on blog post titles that didn’t look
quite right: “Re”, “To”, “ers”, “ey”, “hy”, “ra”, “re” and “ty”. Ligatures like
this help it look more natural: when we write we don’t just stamp out identical
letters regardless of their surroundings, instead they will connect to their
neighbours, or overlap slightly, or even share a stroke.&lt;/p&gt;
&lt;p&gt;I filled these templates in with a Sharpie, as I wanted a fairly informal,
scrap-booky look, and it would also give good solid shapes that should be easy
to pick out of the template. I scanned them with the “Scan Document” function
on my iPhone, and uploaded the PDFs to Calligraphr.&lt;/p&gt;
&lt;h3 id=&#34;iterating-and-tweaking&#34;&gt;Iterating and tweaking&lt;/h3&gt;
&lt;p&gt;The Calligraphr UI allows you to preview the font, but I found it a lot more
useful to just download a copy and use it on a local copy of my website.
That let me test it with real text, and see how it’d look at the different font
sizes I use on the site.&lt;/p&gt;
&lt;p&gt;The first version was not great. Despite the guidelines on the template, I
apparently wasn’t good at sticking to them. Some letters were floating way off
the baseline, and some were sunken below. When those opposites met it looked
terrible. Fortunately Calligraphr has a pretty easy tool to slide each letter up
and down, and scale it up or down if needed, and you can see it next to other
letters as you do it. It took a little bit of time to go through all the
variants of all the letters, but the next version looked a lot better.&lt;/p&gt;
&lt;p&gt;Another tweak I ended up doing was reducing the spacing between letters. The
defaults Calligraphr uses are probably good for a blocky font, but I wanted to
put the letters close together to give it more of a joined-up look.
Again, this is an easy tool to use, you just drag the sides in or out as
desired. While these tweaking steps were probably as fiddly as some of the
Inkscape steps I refused to do earlier, they’re a lot more rewarding as you
see things improving with each one. It’s a lot easier for me to commit time
and effort to improving something that’s already working reasonably, than put
that time and energy into an unknown.&lt;/p&gt;
&lt;p&gt;Later, I noticed that occasionally there would be a huge gap in a title. Not
“the kerning is slightly off” but “there’s enough room to park a bus”. It took
me a while to figure out what was happening: a couple of glyphs
hadn’t been isolated perfectly and had picked up a few pixels from the template
lines at the edge of their boxes. That meant the glyph had a width that covered
the actual written glyph, a big gap, and then the rogue marks. At first, I fixed
this by just adjusting the width, but that left the little pixels floating
awkwardly down-sentence. The proper fix was to use the editing tool and simply
delete them, and then Calligraphr snapped the width back to what it should be.&lt;/p&gt;
&lt;p&gt;These iterations took a while to do, but I just dipped in and out occasionally
over the course of a week, so it didn’t actually feel like too much work. I
quite enjoy the process of refining things, too.&lt;/p&gt;
&lt;h3 id=&#34;result-and-a-surprise&#34;&gt;Result and a surprise&lt;/h3&gt;
&lt;p&gt;If you’re viewing this post on my website&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, you can see the font in the
headers, captions, and a few other places. Here’s how it compares to my actual
handwriting:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/making-a-font-of-my-handwriting/sample.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/making-a-font-of-my-handwriting/sample.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/making-a-font-of-my-handwriting/sample.jpg&#34; alt=&#34;A hand-written line of text saying &amp;#39;Hello World! This is Chris Hand&amp;#39; above the same line of text in the Chris Hand font&#34; loading=&#34;lazy&#34; width=&#34;763&#34; height=&#34;194&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;My handwriting vs my handwriting font&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It’s not close enough to forge documents, but I think it definitely gets
across my style, and that’s exactly what I wanted. It’s surprisingly legible
even at smaller font sizes — I think the weight of the Sharpie helps here — and
at £8 and a bit of manual work was a lot more economical than spending days
wresting with open source tools.&lt;/p&gt;
&lt;p&gt;A few weeks after I put the finishing touches on the font, I got an e-mail
from Calligraphr. As my account had lapsed back to the free version, I was
no longer eligible for the “server-side backup” feature. So what did they do?
They e-mailed me an exported copy! It’s a JSON file with the properties of each
glyph and a base64 encoded image. Not only can I re-upload this to Calligraphr
if I resubscribe, I can probably hook something up to edit it should I ever
need to. I’m blown away by how pro-user Calligraphr’s business practices are.
They’re up-front about pricing, don’t try to get you stuck on an auto-renewing
subscription, and automatically export your data. It’s like a breath of fresh
air compared to the barrage of dark patterns that other websites foist on us.
If you want to make this kind of font, I’d definitely recommend them just
because of how &lt;em&gt;nice&lt;/em&gt; they are.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;And I haven’t changed everything since writing this post… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Coming around on LLMs</title>
        <link href="https://chameth.com/coming-around-on-llms/"/>
        <updated>2025-05-28T00:00:00Z</updated>
        <id>https://chameth.com/coming-around-on-llms/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/coming-around-on-llms/claude-hello.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/coming-around-on-llms/claude-hello.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/coming-around-on-llms/claude-hello.png&#34; alt=&#34;A screenshot of the Claude web UI with the prompt &amp;#39;Say &amp;#34;Hello!&amp;#34;&amp;#39;. The response is &amp;#34;Hello!&amp;#34;&#34; loading=&#34;lazy&#34; width=&#34;176&#34; height=&#34;172&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Claude says hi.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For a long time I’ve been a sceptic of LLMs and how they’re being used and
marketed. I tried ChatGPT when it first launched, and was totally underwhelmed.
Don’t get me wrong: I find the technology damn impressive, but I just couldn’t
see any use for it.&lt;/p&gt;
&lt;p&gt;Recently I’ve seen more and more comments along the lines of “people who
criticise LLMs haven’t used the latest models”, and a good number of developers
that I respect have said they use coding models in some capacity. So it seemed
like it was time to give them another shake.&lt;/p&gt;
&lt;p&gt;The first decision to make was which model to try. OpenAI are no longer the
only player in the game, every tech company of a certain size is now also
somehow an AI company&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. I looked a bit at some benchmarks, and then mostly
ignored them and went with the only company that I didn’t outright hate:
Anthropic, and their model Claude.&lt;/p&gt;
&lt;h3 id=&#34;initial-impressions&#34;&gt;Initial impressions&lt;/h3&gt;
&lt;p&gt;The latest Claude models do feel a lot more “capable” than the earlier ChatGPT
versions I remember, but they also still have a lot of the same problems. At
their heart, they’re still text-prediction models, and still seem to be trained
to predict text that will please the user rather than be factually accurate or
useful.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;One of the improvements that people kept mentioning was the ability for models
to access the web directly. I’ve seen it kick in a bit naturally, and asked
for it explicitly sometimes, and it’s… nothing special? Are people just really
bad at searching the web? Maybe they should try &lt;a href=&#34;https://kagi.com/&#34;&gt;Kagi&lt;/a&gt;? I
feel like about 80% of the time I could have found the information just as
quickly myself, about 10% of the time it searched and then hallucinated an
answer, and the remaining 10% it found something quicker than I otherwise
would have. Those aren’t great results, especially when you consider how
much cheaper and simpler just searching the web is.&lt;/p&gt;
&lt;p&gt;There is another way to trigger web searches in Claude: using “research mode”.
When you enable it, it splits off into different models: a “lead researcher” to
come up with a plan, and then some minions that execute it. It ends up doing
hundreds of web queries in the span of a few seconds. Suddenly I understand why
things like &lt;a href=&#34;https://anubis.techaro.lol/&#34;&gt;Anubis&lt;/a&gt; need to exist. I’ve not used
the “please launch a DoS attack” button since.&lt;/p&gt;
&lt;p&gt;What did impress me, though, was its ability to churn out reasonable-ish
code. It can hack together a bash script as well as I can, and do it far
faster than I’d be able to. Sometimes they even work. That made me wonder
what it would be like doing actual coding with it. Anthropic have a CLI tool
called &lt;code&gt;claude-code&lt;/code&gt;, so I paid them lots of money and gave it a spin.&lt;/p&gt;
&lt;h3 id=&#34;coding-with-claude&#34;&gt;Coding with Claude&lt;/h3&gt;
&lt;p&gt;The first thing I notice about &lt;code&gt;claude-code&lt;/code&gt; is that I really like the
interface. It’s basically an input box in a terminal. It doesn’t force me to
use a certain IDE or do things in a certain way. By default it asks before
making any changes, showing you a side-by-side diff of what it’s doing and
allowing you to provide feedback. If I had to design a way to interact with
a coding agent from scratch, I can’t think of many things I’d improve.&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/coming-around-on-llms/code-session.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/coming-around-on-llms/code-session.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/coming-around-on-llms/code-session.png&#34; alt=&#34;A screenshot of claude-code. I ask it for the permalink for the latest post, and it gives a wrong answer. After prompting it again it gets it right.&#34; loading=&#34;lazy&#34; width=&#34;1121&#34; height=&#34;554&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A simple example of a claude-code session&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The power in &lt;code&gt;claude-code&lt;/code&gt; versus just using the web UI is that it can use
tools. It can query &lt;code&gt;git&lt;/code&gt;, run &lt;code&gt;grep&lt;/code&gt; commands, even use &lt;code&gt;sed&lt;/code&gt; if it wants to
change something in lots of files at once. It’s very good at figuring out its
way around a codebase, even without any explicit instructions. You can see in
the screenshot that with a little prompting it managed to get the permalink
to this post; I didn’t tell it where the posts were stored, or how to work out
the latest, I just told it when it was wrong. If I’d run &lt;code&gt;/init&lt;/code&gt; before it
would have probably picked up on the fact that my posts have custom permalinks,
and noted it in &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But how good is it at actually writing code? It’s like having a keen but not
particularly thorough Junior Engineer at your beck and call. If you give it
a clearly defined task and guidance on how to implement it (and maybe some
feedback as it suggests changes), it’s more than capable of doing it. If you
don’t give it enough guidance it tends to go more off the rails. I tried
having it generate a simple application from scratch with minimal technical
guidance and no review of what it was doing, and it made such a mess of it I
decided it was quicker to throw it away and start again by hand.&lt;/p&gt;
&lt;h3 id=&#34;the-man-behind-the-curtain&#34;&gt;The man behind the curtain&lt;/h3&gt;
&lt;p&gt;Even with sufficient guidance, at times it’s &lt;em&gt;really&lt;/em&gt; obvious that it’s an
LLM generating pleasing-token-strings and not something that genuinely
understands what it’s doing. It will spit out code like this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;sql&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;ErrNoRows&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;chroma-k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Why’s that check for &lt;code&gt;sql.ErrNoRows&lt;/code&gt; there? It’s entirely pointless. There was
no instruction to check for it, none of the existing code checked for it, but
I assume it comes up quite a bit in the training data. But it didn’t
&lt;em&gt;understand&lt;/em&gt; why, so it put the check in, and returned the exact same thing as
if it hadn’t.&lt;/p&gt;
&lt;p&gt;It also occasionally tries to “cheat” or solve problems the wrong way. It
sometimes feels a bit like you’re asking for wishes from a Monkey Paw. “Stop
the unit tests failing”, you’ll say; Claude will respond with a request to
delete the failing test. The man behind the curtain isn’t particularly well
hidden, and the training data and pattern matching often shows through.&lt;/p&gt;
&lt;p&gt;This lack of understanding also makes it very hard to get Claude to use
comments in a sensible way. It absolutely loves doing nonsense like this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;// render form
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-kd&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;chroma-nx&#34;&gt;renderForm&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even with explicit instructions to avoid useless comments, or comments that only
explain what the code does (not why it does it), or other prompts. Again, I’m
chalking it up to a mixture of training data that does that, and lack of any
actual understanding about why a human developer may want a comment to exist.
Of course, there are plenty of flesh-and-blood devs out there that also don’t
comment effectively, so maybe I shouldn’t give Claude too much of a hard time
on this.&lt;/p&gt;
&lt;h3 id=&#34;-and-yet-&#34;&gt;… and yet …&lt;/h3&gt;
&lt;p&gt;So with all those problems, it sounds like it’s just not worth it, right?
Well, not quite. I feel like programming mostly consists of two distinct tasks:
thinking how to implement things; actually implementing them; and then debugging
why you’re off-by-one somewhere. Letting an LLM do the thinking is a big no-go
for me: it’s evidently not good at it, and frankly that’s one of the things
I most like about programming. Having let it loose on small projects, I shudder
to think what the codebases of all the “vibe coded” projects that are popping up
are like.&lt;/p&gt;
&lt;p&gt;For the second stage, though, it’s actually quite nice. If I’ve thought through
how I want something to be implemented, I can feed those steps to Claude and have
it churn out the otherwise not-too-interesting code. This requires far less
time and concentration on my part than writing code, to the extent that I can
be thinking about the next feature, or doing something else at the same time. I
don’t feel like I’m going to end up deskilling myself this way, as I’ve already
formed the idea of what I want to code, I’m just using the LLM to spit it out
faster than I can type it.&lt;/p&gt;
&lt;p&gt;A lot of the most boring bits of coding like implementing CRUD-y operations can
be summarised as “Look at this file/function. Do the same thing but slightly
differently elsewhere”. Claude is great at this, especially when explicitly
prompted like that. You’re basically playing to its pattern-matching strengths,
rather than asking it to come up with anything novel. Most of my prompts tend
to be prefixed with “Look at @some_file.go and @other_file.go.” to cue up the
patterns I want it to use, and I find this works well.&lt;/p&gt;
&lt;p&gt;As for debugging, it’s a mixed bag. It’s sometimes amazingly insightful, and
sometimes just runs around in circles trying the wrong things over and over.
It’s worth asking the question, but I definitely wouldn’t rely on it over my
own abilities.&lt;/p&gt;
&lt;h3 id=&#34;the-future&#34;&gt;The future&lt;/h3&gt;
&lt;p&gt;I’m probably going to carry on using &lt;code&gt;claude-code&lt;/code&gt;, at least for personal
projects. I’ve got so much done that I just wouldn’t have been
&lt;em&gt;bothered&lt;/em&gt; to do if I was doing it all by hand. I very much enjoy being in
the more “diffuse thinking” mindset, planning how things are going to work,
rather than being stuck in the mines digging out SQL queries. After all, who
wouldn’t want an over-eager assistant to work on all their hobby projects?&lt;/p&gt;
&lt;p&gt;Work is a slightly different matter: a private project or even an open source
project that disclaims any liability is different to something I’m being
paid to deliver, and bear responsibility for fixing if it’s not done correctly.
I’m not saying I won’t use it at all, but if I do it’ll be much more constrained
than I would in personal projects.&lt;/p&gt;
&lt;p&gt;As for non-code usages: I’m not sold. My sceptic hat is still firmly in place.
I don’t think chat is a particularly good interface for many things, and
hallucinations are still a big problem despite what people say. I hate the tide
of AI slop that’s taking over the Internet, and how LLM-powered chat
agents are being forced into every random product. You’re definitely not going
to be seeing any AI-generated blog posts from me!&lt;/p&gt;
&lt;p&gt;One topic I’ve not gone into here is the ethical concerns about using LLMs. They
obviously exist, and I do have thoughts, but that’s a topic for another day.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I’m surprised it’s not gone more mainstream: why’s there
no Tesco Value LLM model, yet? &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Simple backups with Restic and Hetzner Cloud</title>
        <link href="https://chameth.com/simple-backups-restic-hetzner/"/>
        <updated>2024-12-06T00:00:00Z</updated>
        <id>https://chameth.com/simple-backups-restic-hetzner/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/simple-backups-restic-hetzner/restic.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/simple-backups-restic-hetzner/restic.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/simple-backups-restic-hetzner/restic.png&#34; alt=&#34;The Restic logo — a gopher with two umbrellas.&#34; loading=&#34;lazy&#34; width=&#34;400&#34; height=&#34;400&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Restic’s mascot, who’s dual-wielding umbrellas to save you from a rainy day.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I have a confession: for the past few years I’ve not been backing up any of my
computers. Everyone knows that you &lt;em&gt;should&lt;/em&gt; do backups, but actually getting
around to doing it is another story.&lt;/p&gt;
&lt;p&gt;Don’t get me wrong: most of my important things are “backed up” by virtue of
being committed to remote git repositories, or attached to e-mails, or
re-obtainable from the original source, and so on. I don’t think any machine
failing completely would be a disaster for me, but it would certainly be a pain.&lt;/p&gt;
&lt;p&gt;This week I finally got around to doing something, and it ended up being a lot
more straight forward than my previous forays into backup-land.&lt;/p&gt;
&lt;h3 id=&#34;restic&#34;&gt;Restic&lt;/h3&gt;
&lt;p&gt;After soliciting a few opinions, the choice of backup software came down to
either &lt;a href=&#34;https://www.borgbackup.org/&#34;&gt;Borg&lt;/a&gt; or &lt;a href=&#34;https://restic.net/&#34;&gt;Restic&lt;/a&gt;.
I’m pretty sure either would have done what I want, but I leaned towards Restic
for a few reasons: it has a more informative website, it’s written in Go
rather than Python&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, and Borg seems to be transitioning between major
releases at the moment&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;The way Restic works is pretty simple: you initialise a ‘repository’, and can
then call &lt;code&gt;restic backup /some/path&lt;/code&gt; and it’ll get backed up to the repository.
Restic handles keeping different backups separate, and only sending data that’s
changed, and deduplicating, and so on. You basically point it at a thing you
don’t want to lose, and it sorts it out for you. Perfect.&lt;/p&gt;
&lt;p&gt;There are equally straight-forward commands for removing old snapshots
(&lt;code&gt;restic forget&lt;/code&gt;) and verifying backups (&lt;code&gt;restic check&lt;/code&gt;). One of the nice things
about modern backup solutions is they support a whole range of backends. I was
originally going to spin up a small VPS to host my backups, but noticed that
Restic supported S3-compatible stores…&lt;/p&gt;
&lt;h3 id=&#34;hetzner-cloud-object-storage&#34;&gt;Hetzner Cloud Object Storage&lt;/h3&gt;
&lt;p&gt;I host my servers with Hetzner, and was going to use them to spin up a VPS as
well. Despite the “Cloud” branding on a bunch of products, they offer reasonable
prices and good service. A couple of months ago, they started offering
&lt;a href=&#34;https://docs.hetzner.com/storage/object-storage/overview&#34;&gt;S3-compatible object storage&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The pricing isn’t totally straight forward, but for continuous use the “free
quota” amounts to 1TB of storage and 1TB of egress a month. Ingress is free,
as is traffic within the &lt;code&gt;eu-central&lt;/code&gt; region (where all my servers are). That
quota is only awarded when you pay the “base price”, though, which is €4.99 a
month. So basically it’s €5 a month for 1TB of storage and enough egress to
fully restore every single byte. That’s better value than any VPS I can find,
much cheaper than Amazon S3, and about the same as
&lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34;&gt;Backblaze B2&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;getting-them-to-work-together&#34;&gt;Getting them to work together&lt;/h3&gt;
&lt;p&gt;Now you’d think making the tool that supports S3-compatible object storage
work with your S3-compatible object storage would be easy, right? Well not
quite. The library Restic uses to deal with S3 backends has some
strange logic for figuring out the bucket name given a URL. It doesn’t quite
seem to work right, though…&lt;/p&gt;
&lt;p&gt;Hetzner buckets have URLs like &lt;code&gt;s3://bucketname.hel1.your-objectstorage.com&lt;/code&gt;&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;,
but just passing that to Restic gives an error that the bucket is not specified.
The docs mention there’s an advanced option to make it use the virtual host for
the bucket name: &lt;code&gt;-o s3.bucket-lookup=dns&lt;/code&gt;. But that… also doesn’t work.
What I ended up doing was specifying the URL as
&lt;code&gt;s3://hel1.your-objectstorage.com/bucketname&lt;/code&gt;, and also passing in the &lt;code&gt;dns&lt;/code&gt;
option. The library then seems to muddle its way back to a real, working URL.
I’m not sure why it works like this: maybe it’s just a weird aspect of S3 that
I’m oblivious to?&lt;/p&gt;
&lt;p&gt;The next fun part is that you can configure Restic entirely by using environment
variables, except for that &lt;code&gt;-o s3.bucket-lookup=dns&lt;/code&gt; argument. That has to go
on the command line. I ended up making a little wrapper script to invoke Restic
correctly:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-cp&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# The password restic will use to encrypt your data. You should generate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# something nice and secure.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;RESTIC_PASSWORD&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;repo-password
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# The path to the repository where restic will save the backup. In our case&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# this takes the form `s3:&amp;lt;endpoint&amp;gt;/&amp;lt;bucket&amp;gt;`. Your endpoint might be different&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# to mine depending on the region your bucket is in.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;s3:hel1.your-objectstorage.com/bucket-name
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# The access and secret key generated in the &amp;#39;S3 credentials&amp;#39; section of the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# Hetzner Cloud Console&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;hetzner-access-key-id
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;hetzner-access-key
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# Pass any arguments on to the restic command, along with the magic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# `s3.bucket-lookup` option we need to resolve the S3 URL properly.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;exec&lt;/span&gt; restic -o s3.bucket-lookup&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;dns &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$@&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in the backup script I just alias &lt;code&gt;restic&lt;/code&gt; to use the script:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-cp&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# This means any time we use `restic` below, we&amp;#39;ll actually execute our special&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# script which supplies all the env vars and arguments needed to find the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# repository. Make sure the path matches where you saved the script!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;alias&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;restic&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s1&#34;&gt;&amp;#39;~/.bin/restic&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# Actually do the backup. Each directory in the list below will be backed up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# separately.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nv&#34;&gt;dirs&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;/some/path/to/backup/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;/some/other/path/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;for&lt;/span&gt; i in &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;dirs&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;[@]&lt;/span&gt;&lt;span class=&#34;chroma-si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;$i&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nb&#34;&gt;cd&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;$i&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; restic --verbose backup .&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# Prune our snapshots. You can tweak the numbers here. Run with `--dry-run` to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# see what effect any changes would have before actually committing to them. &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;restic forget --keep-daily &lt;span class=&#34;chroma-m&#34;&gt;7&lt;/span&gt; --keep-weekly &lt;span class=&#34;chroma-m&#34;&gt;10&lt;/span&gt; --keep-monthly &lt;span class=&#34;chroma-m&#34;&gt;24&lt;/span&gt; --keep-yearly &lt;span class=&#34;chroma-m&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only other notable thing here is that the script changes into the directory
to be backed up. If you give Restic an absolute path, it will create a new
snapshot if the metadata of any folder in the path changes, which is not what
I want.&lt;/p&gt;
&lt;p&gt;If it wasn’t for the S3 URL issues, the whole thing would’ve probably taken
me about half an hour. That’s including setting up the object storage,
installing Restic, and so on. It’s painfully easy. Why didn’t I do this three
years ago?!&lt;/p&gt;
&lt;h3 id=&#34;addendum-a-step-by-step-guide&#34;&gt;Addendum: a step-by-step guide&lt;/h3&gt;
&lt;aside class=&#34;update raised-box&#34;&gt;
  &lt;h5 class=&#34;plain-header&#34;&gt;Update 2025-03-01:&lt;/h5&gt;
  &lt;p&gt;This section was added after the original article was published, following some
helpful feedback. Let me know if you have any problem with these instructions!&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;If you want to do this yourself, here’s a quick step-by-step guide:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in to the &lt;a href=&#34;https://console.hetzner.cloud&#34;&gt;Hetzner Cloud Console&lt;/a&gt;, and
create a project.&lt;/li&gt;
&lt;li&gt;On the “Object Storage” tab, create a new bucket. Note the name and the
endpoint.&lt;/li&gt;
&lt;li&gt;On the “Security” tab, go to “S3 Credentials” and generate new credentials.
Note down the access key and the secret key.&lt;/li&gt;
&lt;li&gt;Copy the first shell script above, fill in the password (you can pick!),
endpoint, bucket name, access key and secret key.&lt;/li&gt;
&lt;li&gt;Run the script with the &lt;code&gt;init&lt;/code&gt; argument (e.g. &lt;code&gt;~/.bin/restic init&lt;/code&gt;). This
will create a new repository, and only needs to be done once even if you
backup multiple machines.&lt;/li&gt;
&lt;li&gt;Copy the second shell script above, making sure the &lt;code&gt;restic&lt;/code&gt; alias points
at the script you saved in step 4. Change the list of directories to
whatever you want to backup.&lt;/li&gt;
&lt;li&gt;Schedule the script to be run automatically, using crontab or systemd timers
or however you prefer.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To check everything is working, you can use the &lt;code&gt;snapshots&lt;/code&gt; subcommand to see
a list of saved snapshots. You might also want to try to restore a snapshot
using the &lt;code&gt;restore&lt;/code&gt; subcommand.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I’m not trying to be a language snob, but given the choice between two
otherwise equal projects one in Go and one in Python, I’ll take the Go one.
I know I’m not going to have weird library issues down the line, and I’m much
more comfortable rummaging around the source. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;With a “Don’t use this in production!” notice on the shiny new version. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;Aside: I really hate URLs like that. What is with the trend for completely
generic domains divorced from the service they’re a part of? &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Generating infinite avatars</title>
        <link href="https://chameth.com/infinite-avatars/"/>
        <updated>2022-12-30T00:00:00Z</updated>
        <id>https://chameth.com/infinite-avatars/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/unique.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/unique.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/infinite-avatars/unique.jpg&#34; alt=&#34;A computer render of the author, with a &amp;#34;UNIQUE LIMITED EDITION&amp;#34; badge&#34; loading=&#34;lazy&#34; width=&#34;256&#34; height=&#34;256&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An example of one of the unique avatars&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I recently added a new ‘about’ section to the top of my
website. Like most about pages, it has a picture. Instead of
a normal photograph, however, you’ll see an AI-generated
avatar. This is admittedly fairly trendy at the minute —
apps like Lensa offer to make you profile pictures if you
give them a set of photos and some cash — but I’ve done
something a bit different.&lt;/p&gt;
&lt;p&gt;You see, there is not just one image that has been carefully
curated, edited, and uploaded. No, the image you see quite
possibly has never been seen before and will never be seen
again. It’s unique. Just for you.&lt;/p&gt;
&lt;h3 id=&#34;background-stable-diffusion-dreambooth-et-al&#34;&gt;Background: Stable Diffusion, DreamBooth, et al&lt;/h3&gt;
&lt;p&gt;You’ve probably heard of &lt;a href=&#34;https://github.com/CompVis/stable-diffusion&#34;&gt;Stable Diffusion&lt;/a&gt;, the open
text-to-image model developed by LMU Munich. Given a text
prompt it starts with a random array of static and repeatedly
transforms it, each step moving away from pure entropy and
towards a real image that befits the prompt&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. It stands
in contrast to competitors like DALL-E and Midjourney
in both the code and the model being freely and publicly
available.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;One interesting side effect of that is that you can take
the pre-trained Stable Diffusion model, and run your own
further training on top. It takes hundreds of thousands of
GPU hours to train such a model from scratch, but only a few
to add some specific tweaks on top. Earlier this year
researchers from Boston University and Google Research published
a paper titled &lt;a href=&#34;https://dreambooth.github.io/&#34;&gt;DreamBooth&lt;/a&gt;
which presents a model for doing exactly that.&lt;/p&gt;
&lt;p&gt;This research has spawned a slew of startups that do the
training and/or generation for you in return for cold, hard
cash. The most popular of these at present is Lensa, a mobile
app that will generate 200 avatars for you in pre-set styles
for £9.99. You can’t change the styles or regenerate any
you don’t like, but from what I hear 200 is just about enough
that you’ll find one or two that you like.&lt;/p&gt;
&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/training.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/training.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/infinite-avatars/training.jpg&#34; alt=&#34;A screenshot of the DreamBooth notebook while training is underway&#34; loading=&#34;lazy&#34; width=&#34;210&#34; height=&#34;113&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The lovely ASCII art shown while training is underway&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;While £9.99 isn’t much money, if you’re technically inclined
then it’s not very difficult to do the work yourself for free.
That also lets you come up with unique prompts, creating pictures
in different styles, with different backgrounds, and so on.
I used the wonderful &lt;a href=&#34;https://github.com/TheLastBen/fast-stable-diffusion&#34;&gt;notebooks from TheLastBen&lt;/a&gt;
that run in Google Colab. The generous free tier offered by Colab
is plenty enough to run a DreamBooth training session, and the
notebook walks you through pretty much everything&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;If you train a custom model then you end up with a weighty
file called a “checkpoint”, which you can provide to most
Stable Diffusion tools to use when generating images. I use
&lt;a href=&#34;https://github.com/AUTOMATIC1111/stable-diffusion-webui&#34;&gt;AUTOMATIC1111’s stable-diffusion-webui&lt;/a&gt;
which not only offers a simple web UI, but also a REST API
for accessing it programmatically. I installed this on my
laptop and spent a happy hour or two generating weird and
wonderful pictures of me.&lt;/p&gt;
&lt;h3 id=&#34;automating-it&#34;&gt;Automating it&lt;/h3&gt;
&lt;p&gt;When I was training the model, I was planning on finding
a single nice avatar to use. After playing around with it
for a while, though, I wanted to expose all the wacky and
unique pictures that it was generating. I came up with the
rough idea of batch generating a number of avatars, then
having a custom webserver that served you one and deleted
it.&lt;/p&gt;
&lt;p&gt;My first attempt at this was to try and run the Stable
Diffusion process entirely on CPU on a server. I’d previously
run an SD generator on my laptop without CUDA support, and
while it was deathly slow it still worked. With this custom
model, though, it took longer to initialise than I was
prepared to wait — and I hate to think how long the subsequent
image generation would have taken!&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I obviously needed something with a GPU, but I didn’t want to
use my laptop as it may be unavailable or doing other more
important things with its GPU like playing games. So I turned
to AWS, and found they have GPU-enabled instances that can be
obtained for reasonable amounts of money. As I didn’t really
care when the batch processing ran, I could use “spot” instances
which offer a decent discount in exchange for only being able
to run when there aren’t reserved instances that need the
resources.&lt;/p&gt;
&lt;p&gt;After much fiddling in the AWS console, I got a spot reservation
set up for a GPU-enabled instance. After waiting a while and not
seeing any instances appear, I checked the logs and found it was
erroring because I was trying to exceed my vCPU limit. Odd. A bit
of googling&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt; later and I discover there’s a separate limit for
that type of machine and the default limit is 0. There’s a whole
mini application in AWS for requesting limit increases, so I
requested a modest increase to 8 vCPUs (the minimum configuration
for the “accelerated computing” images is 4 or 8 vCPUs depending
on the exact type). After a brief wait, Amazon declined
my request:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I am sorry but at this time we are unable to approve your service quota increase request.&lt;/p&gt;
&lt;p&gt;Service quotas are put in place to help you gradually ramp up activity and decrease the likelihood of large bills due to sudden, unexpected spikes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m not entirely sure how you’re meant to ramp up without being
able to run a single instance. There are lots of theories online
about account age requirements, minimum spends, etc, but I wasn’t
willing to jump through inscrutable hoops in order to try to give
Jeff Bezos more money. Instead, I looked at
&lt;a href=&#34;https://paperspace.com&#34;&gt;Paperspace&lt;/a&gt;, a service I’d come across
previously when trying to run a GPU-enabled Windows box. They have
a variety of GPUs on offer, and a lovely API to remotely manage
machines. [If you want to try Paperspace you can use
&lt;a href=&#34;https://console.paperspace.com/signup?R=DSI7ABP&#34;&gt;this referral link&lt;/a&gt;
to get $10 off. In doing so you’ll give me enough credit to generate
around 10,000 avatars. If that’s not a worthy cause, I don’t know
what is.]&lt;/p&gt;
&lt;p&gt;I went a bit overboard investigating the different GPU offerings
and their relative bang for the buck:&lt;/p&gt;
&lt;figure class=&#34;image center&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/spreadsheet.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/spreadsheet.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/infinite-avatars/spreadsheet.png&#34; alt=&#34;Picture of a spreadsheet showing performance and price comparisons for paperspace GPUs&#34; loading=&#34;lazy&#34; width=&#34;1175&#34; height=&#34;453&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A slightly over-the-top analysis of the GPUs offered by paperspace&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The A4000 comes out on top: it’s built on a modern architecture with
a large number of CUDA cores, and is really competitively priced. Paperspace
only give you access to the M4000 and P4000 initially and make you request
access to the higher tier units. I dutifully filled out the very brief form,
and a day later it was approved. At least someone is willing to accept
my money!&lt;/p&gt;
&lt;h3 id=&#34;writing-some-code&#34;&gt;Writing some code&lt;/h3&gt;
&lt;p&gt;After setting up the machine on Paperspace and copying over my custom model,
I set about writing code to handle the generating and the serving. I eventually
settled on having two buckets of images: ones that will be shown to only one
person and deleted on use, and a fallback bucket that will be used multiple times.
The fallback bucket is so that I can limit how many avatars I need to generate
(and thus how much money I pay for GPU time&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;). I set a global limit of one avatar
used every 10 minutes, as well as a per-IP limit of one unique avatar per 24 hours.&lt;/p&gt;
&lt;p&gt;In order to distinguish whether you’re seeing a unique avatar, the server adds
a border around it and a “UNIQUE LIMITED EDITION” label at the bottom. If you
see that text, you’re looking at an image that has never been seen before and
that has already been deleted. The server sends some aggressive caching headers,
so in normal day-to-day operations you should see a different unique avatar every
day you visit the site.&lt;/p&gt;
&lt;p&gt;The generating side is a bit more interesting. It monitors the contents of the
two avatar buckets, and springs into action if they fall below a configured minimum.
It starts the process by calling Paperspace and requesting the machine is started up,
then repeatedly polls the status endpoint until it’s ready. It then generates images
individually using the REST API until it hits the bucket’s configured maximum.
Once it’s done, it asks Paperspace to shut the machine back down.&lt;/p&gt;
&lt;p&gt;Initially I just hardcoded a set of prompts for the generator to use, but they
resulted in a lot of fairly similar images. To make things more interesting, I
started dynamically generating the prompt using a combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A prefix such as “A painting of”, “A sketch of”, “A photograph of”&lt;/li&gt;
&lt;li&gt;In 80% of prompts, an artist reference such as “in the style of Andy Warhol”&lt;/li&gt;
&lt;li&gt;In 30% of prompts, a film reference such as “from the film The Matrix”&lt;/li&gt;
&lt;li&gt;1-10 random suffixes such as “bokeh”, “8K”, “trending in Artstation”&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Initially I had the film and artist prompts independent, but the occasions where
neither appeared in the prompt lead to pretty bad images. Instead, there’s now a
20% chance of a film reference, a 70% chance of an artist reference, and a 10% chance
of both&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;. There’s a list of around 10 prefixes, 70 artists, 20 films and 20 suffixes
which gives a large pool of random prompts.&lt;/p&gt;
&lt;h3 id=&#34;end-results&#34;&gt;End results&lt;/h3&gt;
&lt;p&gt;Everything I’ve described is now live on &lt;a href=&#34;https://chameth.com/&#34;&gt;chameth.com&lt;/a&gt; – if you
visit you might get a unique, never-been-seen before version of me. The code for the
generating and serving is &lt;a href=&#34;https://github.com/csmith/avatargen&#34;&gt;available on GitHub&lt;/a&gt;
if you’re interested or want to replicate this for yourself.&lt;/p&gt;
&lt;aside class=&#34;update raised-box&#34;&gt;
  &lt;h5 class=&#34;plain-header&#34;&gt;Update 2024-12-06:&lt;/h5&gt;
  &lt;p&gt;After almost two years, the novelty of infinite avatars has worn off and I’ve
retired the avatar generator on &lt;a href=&#34;https://chameth.com/&#34;&gt;chameth.com&lt;/a&gt;, going back to a plain old
static avatar.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;To finish off, I ran off a batch of 200 avatars and have selected the most
interesting ones:&lt;/p&gt;
&lt;figure class=&#34;image center&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/avatars.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/infinite-avatars/avatars.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/infinite-avatars/avatars.jpg&#34; alt=&#34;A grid of Standard Diffusion produced avatars of the author&#34; loading=&#34;lazy&#34; width=&#34;640&#34; height=&#34;640&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A selection of avatars produced by the generator&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;As you can see there was one output that appears to be a cat with a ball of yarn,
rather than a picture of me. That seems to happen occasionally when the various parts
of the prompt don’t gel well, but I’m happy with 0.5% or so of the images being
somewhat random! The batch of 200 avatars took just shy of 17 minutes to generate,
which will result in a bill of $0.22 from Paperspace.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;The famous quote from Arthur C Clark comes to mind when I
think too much about how this works: “Any sufficiently advanced
technology is indistinguishable from magic”. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;The only thing it doesn’t help with is &lt;em&gt;finding&lt;/em&gt; enough pictures
of yourself to use for the training data. That’s presumably easier
if you’re more of a “selfie person” than I am. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;There were a lot of differences that could account for the extra
slowness: my custom model was based on the larger 2.1 SD model rather
than 1.5; I was using different software; and my laptop CPU is far more
modern than the server’s. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;In the genericised sense: I used Duck Duck Go. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;One of my biggest concerns here was to avoid putting a “make Chris pay
money” button on the Internet. That felt like a bad idea. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;AKA the random detritus that gets appended to prompts to make images
better in mysterious ways. The English pedant in me hates this nonsense,
but the results when you spam rubbish modifiers are inarguably better than
when using straight forward prose. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;Imagining the Venn Diagrams is left as an exercise for the reader. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
</feed>
