Posts in Tutorial (20 found)
neilzone Yesterday

Automating local backups of UniFi OS Server on Linux with uos-backup

Earlier today, I migrated my self-hosted UniFi controller from Network Manager to UniFi OS Server . One of the annoyances of the new setup is that it does not allow automated local backups - just automated backups to Ubiquiti’s cloud. Fortunately, one can work around this. In the UniFi interface, I set up a new local user, , to use for this automated backup. I am using . is a simple Python scripts which someone has kindly written and shared. I did the following, on the machine I wanted to use to take and store the backups. Get the code: Change to the directory with the code: Edit the python script, for the correct URL, username for my new backup user, and password. Check that the requirements are met: Copy the script to : Make it executable: Create the directory to store the backups. This is the directory specified in the script; you can create a directory with a different path, and then just update the script according Test that the script works: Even though I had just set up a new user, I had managed to get the username and password wrong in the script, and this step helped me debug it. I checked in /var/lib/uos-server/ to check that I had backup files. Set up the systemd services: I then added the backup directory path to restic, so that it gets picked up with my automated restic backups too.

0 views
neilzone Yesterday

Migrating my self-hosted UniFi controller from Network Manager to UniFi OS Server

One of the jobs that has been on my list for a while is to migrate my UniFi controller installation from the self-hosted network manager tool to the new UniFi OS Server tool. The only reason that it was a job at all is because UniFi has decided to discontinue support for the UniFi network manager. Which is probably for the better, as it contained outdated packages anyway. Frankly, I’m not massively impressed with UniFi any more. If I were starting again, I am not sure that I would pick UniFi kit, but I don’t know what I would go for instead. I simply want to run my own controller, without external access or access by anyone else, to control the network infrastructure at home. I did the migration, and it mostly worked. Here’s what I did: I read the Unifi OS Server installation instructions . I also read the Backups and Migration in UniFi instructions. My UniFi controller is running in a virtual machine, so I took a snapshot of that first. If all else failed, I could roll back the snapshot. I backed up the configuration of my existing UniFi network manager configuration. I downloaded it to my local machine. I also backed up the ssh configuration information for my UniFi devices, in line with the instructions: It is also recommended to copy the SSH username and password from Devices > Device Updates & Settings > Device SSH Settings, in case any devices need help later when connecting to the new instance of UniFi Network. I stopped the UniFi network manager with . I followed the Unifi OS Server installation instructions . It will be interesting to see how updates work. The instructions say: Captive portals will be served on port 8444, changed from port 8843 on Network Server. It did not mention that there was also a change to the port to the controller. However, the final line of the set up information showed that it was port 11443. So I changed my nginx proxy config from 8443 to 11443, and reloaded nginx. I could now access the new UniFi OS Server interface. It went downhill from here. I was intending to restore from backup, so I clicked the option for this. It then prompted me to - forced me to - sign in with a ui.com account. I’ve no idea why. It is a local controller, and I don’t want any remote access facilities. Nevertheless, I could not find a way around it. So I did, but I can’t say that I am impressed by this. It then said: We’ve discovered that you already have a self‑hosted UniFi Network installation. Would you like to import your current network settings into UniFi OS Server? But the options were not “Yes” and “No”, but rather “Continue without importing” and “Next”. This was a surprise anyway, as the instructions say: On macOS and Windows, the installer will automatically detect and offer to migrate your existing Network Server setup (if installed in the default location). On Linux, or if auto-migration doesn’t occur, you can manually migrate by installing UniFi OS Server and using the Site Export tool I am running it on Linux, so I did not expect any migration. I guessed that “Next” means “yes”, so I selected “Next”. It took me to a url ending . This was a blank screen. Nothing at all. I waited a couple of minutes, then refreshed the page. It then showed me a page showing that it was “restoring backup”, but the progress bar remained blank for quite a while. It also said that it was restoring to settings from January 2026, not last night’s backup, which surprised me. After a couple of minutes, the progress bar flashed by, and it was done. The import/migration appears to have correctly imported all my devices, and is set up to talk to them. But other aspects of the migration were underwhelming. It did not restore the settings for my mailserver. It was preset to use the “UI Mail Server”. I set it up to use my own mailserver, and it failed, with a useless error message. When I logged in to my mailserver to see what was going on, I saw . It appears that I am not the only person with this issue , albeit with a slightly different setup. They seem to have resolved it by disabling TLS, which is not an option for me. I have not yet got this to work. Even though I had configured automatic backups on the previous Unifi Network Server, they were not enabled on the new UniFi OS Server. I tried to set it up, but I was prompted for my “Ubiquiti SSO account password”. I tried the password for my ui.com account, but I got an error message of “Something went wrong. Please try again later.” Which was no use at all. Having turned off Remote Access (below), I went back to the Backups dialogue. Now, there was an option to download, or upload & restore, but nothing about automation. The info box says that I can schedule backups here, but there is no user interface for that. I took a manual backup. I cannot see a way to do automated backups to my local file system. If this is correct, this is absurd. I may see if I can do something using the command line. *Edit: yes, I can, with python and systemd. See Automating local backups of UniFi OS Server on Linux with uos-backup . “Remote access” is enabled by default, even though I am confident that I did not have remote access enabled before. When I attempted to untick it, it showed a dialogue box: So I disabled it. https://help.ui.com/hc/en-us/articles/220066768-Updating-and-Installing-Self-Hosted-UniFi-Network-Servers-Linux It did not restore my preferred time format (24 hours). I had to turn off analytics, which was on by default. It worked better than I was expecting, but that’s mainly because my expectations were very low. Why the email server and automated backups do not work, I do not know. I will need to investigate these. But at least I am now running a supported controller again. Once I’ve done a scan of the new system with greenbone, I’ll be interested to see what it reports.

0 views

Verifying (simple) C in Isabelle/HOL with AutoCorres

This post details the first steps of verifying a C function in Isabelle/HOL using AutoCorres. It'll go over the basic setup for Isabelle and AutoCorres, what AutoCorres gives you, and how we can verify some basic properties of a function (here, the sum of a list.) I use the latest versions of Isabelle and AutoCorres available at time of writing (24/08/26). This is not official documentation for AutoCorres, nor may it be 100% correct in all places. All the proofs go through, but I do not work on AutoCorres, nor have I used it professionally; most of my experience is hobby verification. However, I have found resources on it are woefully lacking, so I wished to introduce some more. Much of this information has been gleaned from the official documentation (which can be found at the aforementioned,) and this course . When it runs, the slides/similar may be removed for some time - they exist on the internet archive also. This article assumes a little either Isabelle/HOL or general verification knowledge, but I try to explain wherever feasible. A bit of C knowledge is required as well, and so is a little knowledge about program verification - a little Hoare logic, and the like. I'll try to explain as much as I can without being excessively verbose, and much of it is very searchable. You can find Isabelle here . Install as is appropriate for your platform. You can decide whether to put in your path or not. First, pick a directory for your project. It may be feasible to use AutoCorres globally, but I wouldn't recommend it for versioning reasons. Then, AutoCorres can be found by scrolling down here . (It will probably take you here , whereupon you should scroll down to the latest AutoCorres release). You only need the AutoCorres download, as it bundles the C parser. Take that file, and extract it in your directory of choice. Then we need to build AutoCorres. From the directory where you unpacked it, run This'll take a second. Replace paths as appropriate. Note that is not the architecture you are on - it determines how the tool translates various C sizes into Isabelle. This will need to be the same architecture you launch with later. There are some others, and on a related note: It may not all make sense yet, but it may answer some questions. I recommend making a little shell script for this step ( perhaps.) You'll need to invoke Isabelle in a manner similar to the following: Remember that needs to be the same as earlier. Now that we have Isabelle running, we can use AutoCorres to generate Isabelle versions of C files. The manner in which this is performed is long, complex, and interesting - I recommend a rabbit hole evening - but in short, the C parser first translates it to a deep embedding in a language called Simpl, and then AutoCorres takes this Simpl representation and turns it into a monadic shallow embedding best it can. Here's what we'll be verifying today: The use of unsigned will be expanded on later. We'll put it in a C file named . At this point, your directory should look something like or similar. Begin a regular Isabelle theory, importing AutoCorres (and whatever else you want): We then need to let AutoCorres perform its magic. First, we "install" the C file with the C parser: You can use to see what this defines. Of particular interest is . Then, AutoCorres: The idea the C parser and AutoCorres use for verifying C is that it is reasonable to do a very direct translation of C to Simpl, and then a refinement to the monadic representation. The C parser is correct through inspection, and extensive testing. However, the translation of Simpl to the monadic form is verified - there exist Isabelle-checkable proofs that show that the monadic representation, however different it may be, behaves identically to the Simpl equivalent. This makes the monadic form a refinement of Simpl, and is why the things we prove about the monadic forms translate back down to C. This might also take a second. We choose to use . This makes unsigned integers perform modular arithmetic instead of using overflow checks; this has positives and negatives. Read the README for more. Again, you can use to see what this gives you. Of particular interest is . This is the monadic embedding of our function. We then need to enter the locale (think of it as an environment) defined by the C parser and AutoCorres, so your file should look something like: We do our work in this locale. If you haven't already, and . The former is the deep embedding produced by the C parser, and the latter is the result of AutoCorres's shallow embedding. These can be unfolded with and respectively. You can examine the types of things with ctrl-hover (cmd-hover on Macs.) Also examine . The type for the monadic state used by AutoCorres is called (You may see it displayed as in some places.) It's a record containing fields for each type of pointer used; ours only uses , so it only contains information for 32 bit words. We can examine the extract and update functions used in with and . Which monad AutoCorres chooses to embed a function into depends on the function, and can also be configured. This function is simple enough that it can be encoded with purely , but others include , (option with state,) and . Yes, this is a reasonable question to ask. What does it actually mean to verify this function? Generally, there are a few reasons to verify something: We'll look at all three. The not failing example will go into quite a lot of depth, whereas the correctness example will go into much less, only covering broad strokes. This is because the proofs are quite similar, and if you find the former excessively verbose, you might want to skip to the latter. Let's consider what we need for this function to not fail in C. The obvious constraint is that must be defined for all . Using , we can state this as a definition: Now we have our suitable precondition, let's set up our "doesn't fail" lemma. We expect that: We can state this using the combinator . The NF stands for , and it adds the additional condition that our program does not fail in some way during execution. Precisely what we want! It takes three arguments: The arguments are: Here, is used because of the choice of state monad AutoCorres makes. Note that our postcondition takes both the state and the return value . When we use with , will be our state . So, our lemma then becomes: If we have our list defined properly and some property Q, and we run our program, then it does not fail and Q is still true. If you're familiar with Hoare logic, you'll know we probably want to use some sort of weakest precondition reasoning. A weakest precondition is roughly "what is the smallest amount of information we need to know for this to be true", which allows us to simplify our proof obligations. Indeed, AutoCorres provides us with a family of tactics such as and . However, I find it nice to start these proofs by unfolding the function at hand, and applying or similar to get some simplification going. Then we can apply to apply relevant weakest precondition rules automatically. This should leave you with a state something like: I highly recommend using the Query tab of jedit throughout (or equivalents like .) will come in handy, and it's a nice fuzzy search. will do what's on the tin, and will find theorems that could apply. As we have a while-loop, a reasonable step is to add an invariant. An invariant is something that is invariant over the loop - it is always true, at the top of every loop cycle, and right after the loop finishes. This is how we conclude things about what a loop does. Indeed, we can see a theorem of use: Most of the time, the prefixes can be omitted. So first, we add an invariant, and then we can use to transform our into theorems we can work with. The invariants we care about right now are: We also care that this loop terminates, so let's add a suitable measure. is our invariant, and is the termination measure for the loop. Note the type conversion in . Then, again: This'll probably give you a few more normal looking goals. can come in handy again to chunk these down. This leaves me with: The first we talked about earlier - we can solve it by unfolding via , and basic reasoning. For the second, it seems obvious - why hasn't solved it? (If it were on s, it certainly would have.) Alas, it's on s, which as we have chosen to use modular arithmetic, are slightly less nice. It's hard to search for theorems involving as it's so overloaded, but luckily here finds . The final goal is more interesting. The initial intuition might be to use again, but this leaves us with nasty metavariables because of the chaining nature of the binds, and the fact that AutoCorres here is slightly too general. With a little searching we have the following: But we only really need to be identical to . We could instantiate manually each time, or we can make a little helper lemma (which I will do.) Above this lemma, we add Then we can apply twice (There are two binds.) We hence have: It's finally time to start unfolding the definition of so we can crunch down these last few goals. If we hit the first goal with: we're left with which takes out nicely. Tip: I fiddled with this for a bit manually, but if it looks fiddly and obvious, there's a good chance sledgehammer can do it. Finally, the last two are easy: We have a proof of non-failure! This is very exciting. The final proof is as follows: Then, what does it mean for our function to be correct? Well, a reasonable definition is that it produces the output we expect. We then need to figure out what "output we expect" means. We could have also defined the sum of a list as similar to the following recursive function: Note that we explicitly check for 0 to ensure the recursion terminates (We can't pattern match on 0/Suc, as we're working with s.). Unfortunately, this function's termination can't be proven automatically due to the use of a , which Isabelle isn't as good with as its own s. This is why we use a instead of , and we must do the termination proof ourselves: We also delete from the default simp set because it seems to cause some solvers to loop. We can set up our lemma like before, but this time, we use the parameter: We want the result to be equivalent to summing the entire list with our recursive function. We also don't bother to prove that it doesn't fail here. We can begin as before, omitting the step. We then need to annotate with a suitable invariant. A hint: We want the sum at the end to be correct, so a good invariant should capture correctness at every step , which gives us full correctness when the loop finishes. We have to manually a few times as we deleted it from the simp set, but otherwise the proof is very straightforward. Mine came out to be: We could take this one step further if we wanted, and define a bijection between and assuming our precondition, and then also show that our own spec is equivalent to the sum of that list. That'd give us even more confidence our function is correct. I'm personally pretty convinced, but you're welcome to try this yourself! We could then finally prove that if some property is true for the sum of a list, it's true for the result of our program. I won't detail this one; it should follow reasonably easily from the former two. So, what have we (hopefully) learnt? I hope this has been informative! (With less fixing of indentation for the web, sorry): A bit underwhelming for how long it took to explain, perhaps! To prove it never "fails" (what failing means is another question) To prove it produces some desired output To prove it holds some desired property is a , as perhaps expected. has been converted into a . is our state for the function, and carries information as mentioned about the heap. We use the perhaps confusing for pointer addition. You can write this in jedit with . We also need an explicit type conversion , as you can add negatives to a pointer, so the argument is an (this is not unsigned int; that's . here corresponds to Isabelle , which is signed.) If: The list is defined properly Some other property Q is true Then: Our function does return successfully, so That property is still true. A precondition function. A computation function. A postcondition function. (for termination) (for no-failure) (for simplified correctness) How to install and setup Isabelle and AutoCorres What it means to prove things about programs How to set up appropriate proofs How to prove them What tools we have available and how we can search for more

0 views
Giles's blog 5 days ago

Adding diagrams to my static site generator with D2

A lot of the time when I've been writing posts for this blog, I've felt that a diagram would really help. But they're a pain to produce well, and I think I underuse them as a result. I wanted to fix that, and wound up adding D2 support to my static site generator. I think it works pretty well! In the past, I've tried drawing my own diagrams in LibreOffice and exporting as SVG, but my complete lack of artistic skill doesn't help: Asking an AI to do it for me helped in simple cases: ...but with something less standard (there must be a million neural network diagrams in their training sets) it can be really fiddly to get something right. I did some investigations into the various diagram-generating tools out there, and decided to give D2 a go. It has a simple language for specifying what your diagram should show, and the output is pretty nice: Here's the source for that diagram: That looks pretty clear to me! So now, in the source for my blog posts, I have a directory. That contains subdirectories -- by convention, I create one for each post that needs diagrams -- and D2 files. These can be generated automatically when I publish: (Hat tip to Evan Hahn for the method on , which I wasn't aware of.) The flags on the command line took a little bit of fiddling; the just gets rid of the large margins that D2 puts around the diagram by default, but the others are to tell it to use the ELK layout package with particular formatting. Its default layout has curvy lines, and I prefer the closer-to-right-angle ones that ELK provides. Another awkward bit was in scaling; the file that is generated by that command comes out pretty large ( you can see it full-size here ). By default, I allow images inlined into my posts to be as wide as the text, but that would still be too large here. I use to convert the markdown source for my posts into HTML, and there isn't any way to tell it what size an image should be using markdown-ish syntax. So for now, instead of embedding images the normal markdown way, like this: ...for these D2-generated ones I'll just embed a normal tag like this: ...so that I can control the size. Perhaps more work needed there. At some point I may go back and update my old diagrams -- at least, the really ugly hand-drawn ones -- to use this. And a random thought: perhaps it might also make sense to include the D2 source somehow on the blog? I can imagine that it could help with accessibility in some situations, and perhaps also for any LLMs stopping by. Will have to ponder that a bit more. What do you think? Does the D2 diagram look good to you? Or is there a better diagramming package that might work better?

0 views

Forgejo hack: How to set a starting issue and pull request number

I'm currently working on migrating my open source projects from GitHub to a self-hosted Forgejo instance. As part of this effort I often end up looking through the Forgejo source code to figure out if there are hidden ways to configure certain things to my liking when I can't do it on the administration UI. I thought I'd start putting my discoveries in writing here, in case they can help others. So here goes the first one. One of the aspects of the migration that is tricky is how to transition issues and pull requests. What makes the most sense to me is to only use Forgejo to track issues and pull requests going forward, leaving all the issues and pull requests created up to the migration point on GitHub. Of course whether this is a good or bad idea is debatable, but considering all the options I have decided that this is the solution that is going to inflict the least pain on me. The one problem with this approach is that I would end up having duplicate issue numbers, because Forgejo would start creating issues and pull requests all the way back from , and all those low numbers have been used on the GitHub side. So I wanted to hack my Forgejo instance so that issues start from, say, 10000. That way when anyone references an issue by its number I would know that numbers below 10000 are on GitHub and only those above are on my own instance.

0 views
Kev Quirk 6 days ago

2026-08-24 11:01: A friend asked for help setting up a blog, so I went with #Pureblog (obviously)....

A friend asked for help setting up a blog, so I went with #Pureblog (obviously). We went from nothing to fully working blog, with all the customisations she wanted in about 25 minutes. I know I'm bias, but I love how flexible and powerful Pure Blog is becoming. Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️ You can reply to this post by email , or leave a comment .

0 views
Ahead of AI 1 weeks ago

How Claude Watermarks AI-Generated Text

I recently posted a Substack note about Claude’s new watermarking process and implementation. Since it’s such a popular topic and sparked such a lively discussion, I thought it might be interesting to go into a bit more detail when explaining how it works. Instead of the usual text article, I recorded a little lecture on the topic (to change it up a bit from my usual articles). So, below is the video along with a transcript. Originally, I planned to make 10 slides and record a short 10-min video. However, while putting it together, I added some crucial details here and there, resulting in >50 slides and a 48 min recording. I hope that this now explains it well, though! Happy watching! I also have a YouTube version if you prefer using the YouTube player And here is a link to the slides Subscribe now Note: The transcript below is slightly edited and cleaned up for readability but preserves the overall order and flow of the video lecture above. Slide 2 of 52, time stamp 0:00 Hi everyone. So, a few days ago, Anthropic announced that they will watermark the text outputs of their Claude models. I then did a social media post briefly explaining how that works. And yeah, this was quite the popular post. So not the watermarking itself was popular, but I guess the explanation or the mechanism behind it. Then, it might be worthwhile expanding this a bit to explain it in more detail, because this post only had one figure, and there were a lot of questions and discussions. So, I thought, well, let’s make a few more figures. I actually originally planned to do like 10 slides and walk you through it. It ended up being 50 slides, but I hope this really explains how this watermarking technique works well, how watermarking itself can fail or be removed, and so forth. So I think it might be an interesting topic because a lot of people use LLMs these days and also consume a lot of text on the Internet that might be generated by LLMs. And now there’s going to be this watermarking, and there’s this, I guess, fear of watermarking making text worse, or what’s actually the benefit of this watermarking? And so what does it mean? And I think if we understand a bit better what watermarking is, that goes a long way, and then we can make up our own minds about whether that’s a good thing or not, and so forth, like the pros and cons. So, my goal here is really to explain how the underlying mechanism works and how they are going to implement this type of watermarking, text watermarking. Slide 2 of 52, time stamp 1:41 It’s also a great example to illustrate why understanding things from scratch is actually quite useful. This watermarking technique is also a nice way to explain how conventional models or LLMs in general work under the hood. So yeah, you may know I like doing things from scratch. Like, I have my books: Build a Large Language Model From Scratch, Build a Reasoning Model From Scratch. I have some articles labeled from scratch. So, for me, “ from scratch often includes coding. So this one will not be coding-related, but coding from scratch is actually a very, very useful technique because it really helps you understand how something is implemented. And then from that we can derive our understanding, figures, concepts, because if we don’t really implement things, if there’s no code, it’s really sometimes ambiguous. And of course, you know, as I realized, not everyone is coding from scratch anymore. Like back in the day, coding something from scratch was all we had. I mean, there were only humans coding. Nowadays, coding can be done by LLMs. However, that doesn’t mean reading code is no longer useful, because it carries a lot of information. So in this case here with this watermarking, spending some time coding an LLM from scratch really makes you realize how this sampling inside is implemented. We still have some relevant code snippets. And then that really, in turn, helps us understand, oh, the watermarking is applied at this position, and this has so-and-so consequences and so forth. So I think even though people may not be coding from scratch, at least not all the time anymore, it is still useful being able to, let’s say, build something from scratch for educational purposes to understand something deeply and then also for research purposes to manipulate this in a transparent way that is not hidden away in tons of layers of abstraction. But that aside, I think it’s just a coincidental nice relationship here because, for this slide deck, I actually used a lot of figures from my from-scratch coding materials. Slide 3 of 52, time stamp 3:58 So a few days ago (this is August 14), there was this article, How Claude’s Text Watermark Works, and there was this article here; it’s just like a screen recording, so it can have everything in the slides, but there’s plenty of detail. They updated it actually a couple of times, so originally when I read this, it was a way shorter. Still, it is very, I guess, conceptual; there’s like this overview, and there’s, I mean, there’s not a single figure in there. And so it’s kind of still hard to understand what they’re trying to do. So they explain a lot about why they’re going to do it, but they don’t explain how. They’re linking to one paper somewhere there, which is very technical also. So I do think it makes sense maybe to take a step back and start at the beginning to kind of understand what they’re trying to implement here with this watermarking technique. And so the motivation, by the way, of watermarking is for them to identify if someone posts some text that they can say, oh, this text was generated by our Claude Opus 4.8 model, for example, so that they have a way to tell, OK, this text is AI-generated because it carries this watermark. And this watermark is invisible to users, so only they can decode it and find out whether the text has their watermark. Why can only they do it? We will get to that later in this (hopefully not too long a video), but one thing at a time. Slide 4 of 52, time stamp 5:35 So I wanted to start with a brief prelude to explain how text generation works in LLMs, because based on that we can then more easily understand how the watermarking works and that this is actually not a huge, expensive thing on top of it. It’s really just like a minor, I guess, tweak inside the regular text generation process. Slide 5 of 52, time stamp 6:01 So when we are using something like ChatGPT, for example, let’s say I ask the question, the capital of Germany is, and yeah, ChatGPT or other LLMs, so this is just like an example would, for example, answer “Berlin”. So here, in this case, it’s generating two tokens, like “Berlin” and the period. But for simplicity, let’s assume it’s generating one token. So the next token is the “Berlin” token. How is this token generated internally? What is happening under the hood when we type something here like the capital of Germany is and receive a token like “Berlin” back? What is actually going on there behind the scenes? Slide 6 of 52, time stamp 6:41 So in the next couple of slides, I want to briefly talk about what happens under the hood when this next token is generated. Slide 7 of 52, time stamp 6:50 So assume again that our prompt is the capital of Germany is. And the first step here is to convert this into token IDs. So tokenizing it and converting it into token IDs is one of the main steps at the beginning. This is outside. It’s not inside the LLM; it’s outside of the LLM. So we are simply converting the text into token IDs. It’s just a format that embedding layers can work with. Slide 8 of 52, time stamp 7:22 And then this passes through the LLM. And the LLM gives us a score distribution for the next token. Slide 9 of 52, time stamp 7:31 So again, this is just like a brief overview of how LLMs work internally. So I’m not covering the LLM machinery itself. I talked about it many times in my other From Scratch LLMs videos and books. The important part is that when we generate the next token (for example, “Berlin”), we have, at this point, a distribution of scores. So this is the output produced by the LLM. Here in this case, we’re looking at logit values. So these are just scores from minus infinity to plus infinity, like a range of scores. Here’s an example, ranging from about -8 or -9 to 20. We could convert these into a probability distribution, but technically, it’s not strictly necessary depending on how we sample. But so you can think of the logit values as the raw scores. And the raw scores go over the entire vocabulary. Slide 10 of 52, time stamp 8:39 That means every possible word that the LLM could generate. Now here, in the vocabulary index, a certain value (index position 19,846) receives the highest score. So I spread out the distribution. If you would run this prompt through an LLM, you would even see something more extreme: that everything is, like, very, very, very close to zero. And “Berlin” would probably be much, much higher even. But just to show you a few, you know, like peaks here so it looks a bit more interesting, I kind of zoomed in; in and spread out the distribution a bit. Now here, “Berlin” is the highest score because you can think of it as the most, I guess, probable or plausible next token if I have a very specific prompt like this. So the other ones, I mean, it could be something like Hamburg or Munich that the LLM might guess incorrectly. But nowadays an LLM should be fairly certain that “Berlin” is the correct answer here. You are also seeing here the vocabulary index. So that’s like over the whole vocabulary. Nowadays, LLMs have like 250,000 possible tokens as output. I’m truncating it here from 19,800 to 19,900 because there’s just so much space here on this slide. If I would have a very realistic vocabulary of 250,000 words, everything would be so narrow that we would barely even be able to tell or see anything on this distribution. So this is just truncated for educational purposes. The important point is that in regular text generation, we get this score distribution. Now, what we do is look at the highest score. Slide 11 of 52, time stamp 10:33 I will get into more detail later on how this is selected. So it’s not necessarily precisely the highest one, but for simplicity, assume we are taking the highest score here. And in this case, it’s 19,846. Slide 12 of 52, time stamp 10:52 And this score is then detokenized, and we get “Berlin” back. So that is the process here on this slide: from an input prompt to conversion into token IDs and tokenization, passing it to the LLM, getting this score distribution, getting the next token, and converting it back into text. Slide 13 of 52, time stamp 11:12 And then this text is appended to the input. So if we have a question that requires multiple output tokens, we keep going in this loop until the answer is complete. That usually means that the LLM generates an end-of-text token, for example, here. For simplicity, I’m showing you only one iteration where it generates one token. But yeah, as I said, it would kind of continue like that, where we are feeding back the modified input to the LLM for the next round. Now, how do we actually sample this next token here? Slide 14 of 52, time stamp 11:44 I briefly said, well, we could just technically select the highest one, the one with the highest score. This is called greedy decoding. That’s one way to do it. But most LLMs, like if you use them, they don’t do greedy decoding where they always pick the highest one. Because if you ask it on some other prompt, it might not be what we want to always have the highest score, because then it would memorize the training data. It would always kind of give the same response and so forth. So we actually often want some variation in the outputs, but not so much that it generates random stuff. So how it works is that, when we sample here from this distribution, we first typically convert it into probability scores. Slide 15 of 52, time stamp 12:28 So here I just have these scores shown in this plot. I’m just using NumPy for simplicity; whatever tool you use (e.g., PyTorch), the same concepts apply. But let’s assume we have the scores here in NumPy. So what I would do is I would compute the softmax. Technically, I would use a softmax function implemented in Torch or PyTorch, for example, that is numerically stable for both large and small values, including very high positive values, very low positive values, and very high negative values. Here I’m just writing it out like that. That’s the canonical softmax, just to make it a bit more readable. But the details don’t matter here. Slide 16 of 52, time stamp 13:20 What matters is that after this conversion, the scores here, I mean, there’s only so much space on the slide, but the scores here, they would add up to one. So it’s essentially like a renormalization. So they would be normalized to sum up to one. That’s all that the probability conversion does: the softmax conversion. So then once we have these probabilities, we can use a random number or, like, a random sampling algorithm. For example, here in NumPy, we could use the choice function or method. So this is with a specific random seed we are passing to the vocabulary indices. And then, and that’s the important part, we are passing the probabilities as the weights. So, these, essentially, yeah, are like: “How likely is a certain token to be selected?” So, for example, if “Berlin”, after this normalization step, the softmax step, has a 99% probability and the other ones together have a 1% probability, then if we would sample 100 times, 99 of the times, we would get “Berlin”. In realistic LLMs, for example, that are well trained, “Berlin” might receive a probability of 99.999999 or something like that. So you’re almost certainly always sampling “Berlin” because it’s very confident that the answer is “Berlin” in this particular case. So yeah, that is how we would sample from this distribution. There are modifications like top-k sampling or top-p sampling where, let’s say, just for simplicity in top-k sampling, we would select the top 100 tokens and then apply this random choice only to the top 100, the 100 highest-scoring ones, so that we don’t get nonsense tokens in there. For this example, it doesn’t really matter. I mean, it’s just like another thing to explain, so I’m skimming over this. So you can maybe assume that this is already the top 100 tokens using top-k or something like that. Slide 17 of 52, time stamp 15:37 And so, for example, here’s an example. If we sample 10,000 times with a probability of “Berlin” being very high, 99.9, we would sample “Berlin” 9,997 times, sample the word “Hal” twice, and one “Moh”. And these are basically nonsense tokens. It rarely happens that, in this case, the LLM might produce nonsense because, as I mentioned before, I spread out this distribution a bit to make it more interesting. A real LLM would probably, 10,000 out of 10,000 times, sample “Berlin” because the probability of “Berlin” is so high. But this is for illustration purposes. Slide 18 of 52, time stamp 16:21 Now we briefly talked about how LLMs work under the hood, which I think is kind of an interesting concept in itself. But I’ve talked about this many times before, so I don’t want to bore you. I just wanted to set up some context for now, explaining how this watermarking works. Slide 19 of 52, time stamp 16:41 So, we mentioned that we select the highest-scoring token when sampling. Or we use this probability sampling, which will lead to one of the highest-scoring tokens being selected most of the time. Now here’s another example without watermarking. I changed the prompt slightly. Now the prompt is: today’s weather is “cold,” and a possible answer could be, for example, “gray” or “overcast”. So in contrast to the “Berlin” example, I would say “gray” and “overcast” kind of are interchangeable. They are both reasonable next tokens for this prompt, given the goal of completing this text or writing the next token. So it’s almost like a coin flip which one we want to select. There is not really an objectively worse one of one or the other. So when we do the random sampling, because they also have relatively high scores and their scores are similarly high since they are both plausible tokens, we might get one or the other. So almost half of the time we would get “overcast”, and almost half of the time we would get “gray” if we repeat the sampling multiple times. And that’s how LLMs often end up with different answers if you provide the same prompt. If you use the same prompt and you ask the LLM multiple times, you often get slightly different answers. And that’s because at certain positions, two possible tokens are almost equally likely, so it will choose one or the other. And that token would then influence all subsequent tokens, and so forth. Slide 20 of 52, time stamp 18:25 Now, I wanted to briefly talk about random number generation. So, for example, if we use a random number generator like this, it will generate a random sequence of numbers. If I run it again, the sequence of numbers is different here. So you can see every time we produce five numbers, they are different. If I set the random seed here, like one, two, three, and I run this multiple times, we still get random numbers, but they are now all the same, right? So they are still random. If we use a random seed, we still get random numbers that are different from each other, but they are reproducible. So whether we use a random seed or not, we still get random numbers. But with a random seed, we get a reproducible sequence of numbers. So keep this in mind: this is just like a little primer, and we will use this concept in a few moments. Slide 21 of 52, time stamp 19:26 So, for example, I mentioned before that we might get either “gray” or “overcast” if we randomly sample. Now, if we use a specific random seed like 42, we would always, for example, select “overcast”. I mean, it’s still a random selection, but we make it deterministic. In this case, given this prompt, the model will always select “overcast”. Slide 22 of 52, time stamp 19:49 If we use a different random seed, the model might select “gray”. Every time we sample, it will always select “gray” as the next token. So it’s still random sampling, but we are making it deterministic based on the random seed. Slide 23 of 52, time stamp 20:04 So, in watermarking, Claude watermarking is kind of like the idea that it sets a random seed. But this random seed, instead of being like a number that is fixed based on, I don’t know, someone writing down a fixed number, they’re using a secret key that is essentially like an API key, a secret key, and from that key, together with the four previous words, they derive this random seed essentially. But the idea is that if I go back one slide, it’s the same as here: there’s essentially a fixed random seed, and that random seed always selects the same next token. Okay, so instead of using random seed 99 here, for example, they have a secret key and also use information about the previous tokens to derive this random seed. But more on that later. Slide 24 of 52, time stamp 21:06 So the idea is that watermarking makes the text generation more deterministic in certain positions. So, for example, if we have these plausible texts on the left side. So if I have a text that says, > The weather today is cold and I may either pick “overcast” or “gray”. And the next sentence could be, > and then “light” or “gentle” They’re both interchangeable again. > And then breeze is “moving” or “blowing” through the trees, and the streets seem “quiet” or “still”. Which means basically I could say either “quiet” or “still”. So there are certain positions in the text where we have token choices where they are almost equally likely, like we have seen before. So that means if we are, this is without watermarking, if we are running the prompt, or given the prompt through the LLM, we might sometimes get this answer here, sometimes this answer, and so forth. And based on the number of positions, we might have 128 possible answers here. And of course, the longer the text, the more positions we have where we can have terms interchangeably, the more combinations, or the more output texts, there are. So, for example, again, one possible output text could be > The weather today is cold and overcast. A light breeze is moving through the trees, and the streets seem quiet. I think I’ll stay home and read a book with a cup of tea. So that is one possible text. Another possible text is > The weather today is cold and gray. A gentle breeze is blowing through the trees, and the streets seem still. I think I’ll stay inside and read a novel with a mug of tea. By the way, it’s also actually raining outside. I don’t know how good this microphone is, but it’s kind of a very fitting context here. But yeah, the bottom line is that you can see there are two very reasonable texts here being generated, and there are more combinations. So they are all reasonable. There isn’t one that is necessarily better than the other. They’re just, you know, slight variations. And if we don’t use watermarking, we might get either one, or it’s just random, right? Because of the random sampling, we might get one or the other. Slide 25 of 52, time stamp 23:31 Now, if we fix the random seed, as I mentioned before, for example, if the random seed is 99, we might always get this text here. So, using a random seed, we can kind of fix which answer we get, because then the random sampling is still random, but it’s deterministic in the sense that it’s reproducible. It’s always going to be the same then. Okay, so that is still without watermarking, now with a random seed. Slide 26 of 52, time stamp 23:59 And the watermarking is essentially doing the same thing. Now, instead of just using a simple random seed, they have a so-called random key, where this random key is involved in selecting the text, essentially. But what we can already say is that, in the Claude blog post, they say the watermarking shouldn’t make the text worse. If we look at this mechanism, yeah, it makes sense why it would not make the text worse. By the way, I’m not defending watermarks here. I’m just trying to explain. So please don’t kill the messenger here. But what I’m trying to say is that the watermarking is nothing else for the end user than fixing a random seed and making this sampling kind of deterministic, if that makes sense. Slide 27 of 52, time stamp 24:47 Okay, so the summary so far is without watermarking. We often sample without a random seed because I know most people don’t even use one. I honestly don’t think you can necessarily do it with the Claude and OpenAI APIs. I know you can do it in Ollama, but I also always had some problems with that because I used Ollama in one of my books for the bonus material to generate some texts. I was fixing the random seed, but it still wasn’t always deterministic, and so forth. So it’s tricky. Your mileage may also vary, depending on the software version and so forth. Anyways, so without watermarking, we have this random sampling. With watermarking on the right-hand side, we still have the random sampling. But in addition to just a random sampling being fully random, we have this watermarking key. And this watermarking key is passed to the random seed generator to set a specific random seed, making this deterministic. But it’s essentially very similar, and like I mentioned, there’s a lot of benefit in terms of understanding things from scratch. And now we know essentially where this watermark is applied to. So this is essentially applied to the sampling. It’s not applied inside the LLM, which is actually cool knowledge. So they don’t need to train a new LLM for that. They can just use an existing LLM, and they just apply it at this sampling stage. They don’t have to retrain anything or anything like that. So yeah, that is actually interesting, right? Slide 28 of 52, time stamp 26:21 But we are not quite done yet. I would also like to talk about how we can understand or see whether text is watermarked. So detecting the watermark is only possible if we have access to the key. So, for example, if we have these different texts, and essentially, after the text was generated, you find some random text on the internet (for example, you find this text number four here on the internet somewhere), you want to know: is this watermarked? Well, it’s impossible to know because, in order to know, you would need the watermarking key. You need this scoring function, and then you have to score basically the text with a scoring function. And then the idea is that if the score is above a certain threshold, then the text is watermarked. Otherwise, it’s not watermarked. But as the end user, we can’t do this because we don’t have this key. So the key is not available to us. Only Anthropic will have the key. However, in this blog post, they mentioned that they are providing it, of course, or they’re going to develop an API for that that they will make available. I don’t know. Honestly, I’m not affiliated. I don’t know the details. I was just reading this in this blog post. That’s all I know. So that API might as well be private for some companies, like, let’s say, X or Substack Notes, when they want to label AI-generated posts. They may make it public for end users to use. Who knows? We will have to wait on that. But yeah, so the bottom line here is that watermark detection is only possible if we have this watermarking key or, of course, the API that they are going to develop. Slide 29 of 52, time stamp 28:03 Now, removing the watermark is interesting. So now that we know how the watermarking works, we also know the shortcomings. I mean, this is really highly dependent on specific tokens in certain positions. So, for example, in this given text, if these colored words or tokens are the watermarking positions, we know that we could remove the watermark by editing this, right? If we change all the words at these positions, we would be 100% able to defeat this watermark. Now, the problem, though, is that we don’t know, right? Slide 30 of 52, time stamp 28:41 So we don’t know where these words are because we haven’t generated the watermark. So we don’t know which positions to look at. So the practical scenario here is that we could just randomly edit the text. So we would randomly change a few words and hope that we change enough positions to edit the watermark. So that would be one way to remove it. And since we also don’t know which are the highest-scoring ones, because that would require us to have access to the LLM and rerun the prompt through the LLM to find out which words are the highest-scoring, we can kind of only guess. So for example, we might say, oh, we replace “overcast” with “cloudy” because we don’t know that “gray” was high-scoring, you know? So in this case, it might be intuitive to say “gray”, but there might be cases where it’s not so intuitive. So what I’m trying to illustrate here is just some general text editing where we are modifying positions, but we are still kind of guessing what a watermark position is. So since we don’t know, we added just a few words here and there. And if we added enough words, that would also defeat the watermark. Slide 31 of 52, time stamp 29:59 So yeah, that was the watermarking in a nutshell. I mentioned that there is a scoring function to find out whether something is watermarked. And I want to do it as a bonus here. It’s already a long video, but as a bonus here, I wanted to briefly also explain how this scoring function works because that is also interesting information. It’s a bit complicated. It’s not essential to understand how the scoring function works. But the reason why they do it the certain way they do is to make the detection cheaper. Because otherwise, if I go back one slide or two slides, if you wanted to check if something is watermarked, if even they wanted to check, they would have to rerun the prompt to get these scores and then apply this watermarking random seed to get this text and then compare. And that would be very expensive because then essentially every text you want to compare, you would have to rerun the LLM. You have to know which LLM, and that would be really unfeasible because you often also don’t even know the prompt, right? So yeah, so they have like a trick that they use to, yeah, I would say, modify the sampling so that you don’t use or don’t need the LLM later on for the scoring stage. And in the blog post, they mentioned that they derived this method from a paper. It was a Nature paper, and this method is called SynthID-Text. So that was like a paper that came out maybe one or two years ago. It was by Google, and they use a similar technique they call Claude watermarking. I don’t know, sorry, I don’t know if they use exactly that technique, but that’s the one they mentioned. Slide 32 of 52, time stamp 31:39 So how does it work? So before we looked at the slides, we looked at the regular, let’s say, overview here, where we have some text. We put it through the LLM. We get this logit distribution and then we sample from the distribution and get the output token. And here, during the sampling, we use the watermarking key and the random seed generator. So this is still correct. This is still what’s going on, but there is a bit more, I guess, nuance to how this token is sampled. So they’re not just using, let’s say, NumPy’s random choice. They’re using something a bit more sophisticated here. Slide 33 of 52, time stamp 32:16 So assume, again, our context is “the weather today is cold,” and we want to generate the next token. So, for example: “gray”, “overcast”, “gloomy”, “cloudy”. “Gray” is 50% probability, “overcast” is 30, “gloomy” is 15. Let’s say “cloudy” is 0.05 and the rest is, let’s say, 0. Here it looks, of course, a bit different. Let’s say that’s “gray” and “overcast”. I’m just reusing this figure. But now imagine these are the most likely ones, like “gray” and “overcast”, and everything else is just very small, except “gloomy” and “cloudy,” maybe. So essentially, think about just a very small vocabulary for this example of four words instead of all these 50 words here, just to make it even simpler. Now, as I mentioned before, we could use ‘sNumPy’s random choice with these probabilities to sample the next token. Slide 34 of 52, time stamp 33:13 And we could use the watermarking key with this random seed generator to make it deterministic and get the certain watermark that we want. But as I mentioned before, this would be very expensive. Not the sampling itself. That doesn’t matter. This is pretty cheap. But the detection later on would be very expensive if we are trying to check random text on the internet. Slide 35 of 52, time stamp 33:35 So instead, what they use, they also use it during the sampling, during the generation, so that it can be reused later during detection. What they use is called tournament sampling. So this is instead of using something like random choice, they use a concept called tournament sampling. And so how does that work? It might look a bit complicated, but it looks really more complicated than it really is, to be honest. So you might have to, I guess, stop the video at some point and just sit with the figure a bit. But I think it is actually simpler than it looks like. It’s like once you get the hang of it, it’s pretty straightforward. But let me try to explain here. So what we have is we have still this context, and then we have these probable or plausible next tokens with these different probabilities. Now they have something they call random watermarking functions. Slide 36 of 52, time stamp 34:35 Here we have three watermarking functions, G1, G2, and G3. In reality, they might have 30, 50, or even more. Here I’m just using three because that is simpler on this slide. It’s just smaller, you know, like it fits better on the slide. Now, if we look at this word “gray”, this might give us a signature 101. With that, I mean, if we use this watermarking key to generate this random seed, and we have three functions, G1, G2, G3. If I put the word “gray”, what I’m skipping here is that usually you put the word “gray” together with the four or three previous words from the context. So it’s “cold” and “gray”. If I put that into G1 together with this watermarking key, I get the value one. Why? Well, that’s just how this function works. It’s like a random function. The random function either returns zero or one. In this case, with this random key and this token, it returns one. With the same key, but a different function, you get the value zero. And then here you get a one again. So if we have more functions (of course, 30 functions), this will be a very long string of ones and zeros. Slide 37 of 52, time stamp 36:00 It’s basically like a bit string, like if you have bits of zeros and ones. Okay. So this is for “gray”. So we get the signature 101 through using these watermarking functions. Now we can do the same thing for all the other ones. So we can do it for “gray”. We can do it for “overcast”, “gloomy”, and “cloudy”. So each one has a different signature here. So, for example, “overcast” is zero, one, zero. “Gloomy” has zero, zero, one. “Cloudy” has one, zero, zero. Okay. So we have these bits here now. The next step is a so-called tournament sampling where we just pair them. Slide 38 of 52, time stamp 36:39 Like, you know, like a soccer tournament, the knockout (KO) stages, or like the playoffs in American football, you always have two teams playing against each other. And that’s kind of like the same idea. We have a pair of tokens, and they’re playing against each other, essentially. And the scores, they come from these functions here. So we start with the first function in the first round. So we have “cloudy” and “gray”. So we look up here: “gray” is a one and “cloudy” is a one. Okay. So one and one. “Overcast” and “gray”. So “overcast” is zero, “gray” is one. So we have zero, one. “Gloomy” and “overcast”. So here we have “gloomy” zero, “overcast” zero. So zero, zero. And then we have “gray” and “gray” again, because we are running out. So we don’t have enough of the others. So we have one duplicate. So this is chosen randomly. And so you have one and one here. Now we look at the results. So this is a tie. In the case of a tie, we also select randomly using, you know, the random seed and the watermarking key. So here, “cloudy” survives. And from this one, G1 is, according to G1, “gray” is the winner because it has the one. So “gray” survives. And then here, “overcast” and “ gray “ are a tie, randomly selected, and “gray” also randomly selected. So we have now “cloudy” and “gray” and “overcast” and “gray”. And we play the next round in this tournament. So in this next round, we use G2. So according to G2, “cloudy” has a zero here. “Gray” also has zero. “Overcast” has one. And “gray” also has zero, sorry. And so, the next stage of the tournament again. Slide 39 of 52, time stamp 38:24 So we have a tie. We randomly select “gray”. And here we have “overcast” as the winner. And so we have “gray” versus “overcast” in the final. And then we look again at the scores. So “gray” has a one. “Overcast” is a zero. So “gray” is the winner. And that’s how the token “gray” is sampled. What is the watermarking key doing here? So the watermarking key, if I go back a few slides, is selected for generating these scores using these random watermarking functions. So the watermarking key determines essentially what values we get at these stages. So the watermarking key is still very important. Otherwise, these signatures would look different. Slide 40 of 52, time stamp 39:11 So we now have sampled the next token. And that’s just how this modified sampling procedure works. We could have used NumPy’s `random.choice`. But the shortcoming of that is that if we want to score random text on the internet, we would have to rerun the LLM. With this technique, we don’t. I will show you in a moment. So this technique sounds like really weird and cumbersome, but it has the advantage that we can now score random text more easily without having to rerun the LLM. So it’s essentially just to make the detection easier and cheaper. Slide 41 of 52, time stamp 39:43 Slide 42 of 52, time stamp 39:48 So, for example, if we have a new text. So I’m just using the same text here, but let’s assume it’s new text. So this is after the sampling, when we are scoring. And let’s say we are discovering this text on the internet. And the text is the weather today is cold and “gray”, and we want to know if this is LLM-generated or not. So we would, or Claude/Anthropic would, have the watermarking key and these functions: G1, G2, and G3. And it would put this text through these functions. For the one position here for “gray”, we would get 101, similar to what we got during the generation process. So this is the same as before. And this has, if we add up these bits, two bits, right? One and one here. So it has two bits of information, let’s say, for simplicity. This is just a really simple illustration. But let’s assume we get a score of two here for the “gray” in this position. If we had a different word here, “overcast,” in this position, we would get one if we get “gloomy,” like we also have one, and “cloudy” one. So I’m just summing over each row here, right? So that’s just like a score we would get at each position. And here I’m only looking at the last position. If I would do this at other positions, I would get a different score at different positions. So, for example, let’s assume at the first position I get a two. Here I get a two. For “today”, I get a three. For “is”, I get a two. “Cold”, two. And “gray”, three. So here I’m applying these watermarking functions as I’ve shown on the previous slide. Slide 43 of 52, time stamp 41:33 And I’m just adding up these numbers across the three functions. And the watermarking functions are very cheap. So you can just quickly run them on the whole text and get these scores. And then based on that, I can compute the average bits. So if I just average over all these values here, let’s say I get 2.23. Slide 44 of 52, time stamp 41:55 Now, if I have slightly different text, so here I swapped “today” with “now” and “gray” with “overcast”. These now get a score of one and one. And if I average over this whole string, then I get a 1.71. And so for that, I don’t need an LLM. All I need is the watermarking key, the random seed generator, and these functions, G1, G2, and G3. And that’s all I need. I don’t need the LLM. And I can get this score here. And what they do is apply a threshold. Slide 45 of 52, time stamp 42:27 So, for example, I mean, they don’t use this exact threshold. But for example, we can say if the score is greater than two, then the text is watermarked. If the score is smaller than two, it’s not watermarked. So here, if the score is greater than two, it’s a yes. So yes, this is watermarked. In this case, 1.71 is not greater than two. So this text is not watermarked. Okay. So that’s just the way we can then detect whether random text on the internet is watermarked or not. It’s essentially just applying these watermarking functions and then averaging over the scores and applying a threshold. Okay. Slide 46 of 52, time stamp 43:13 So again, the tournament sampling is mainly to make detection easier and cheaper. We could also use something like NumPy’s random choice with a random seed or to make the sampling deterministic. But then again, it would be hard to score any text on the internet. Slide 47 of 52, time stamp 43:30 So yeah, the summary is still the same, though. The thing that is different between no watermarking and watermarking is that we are controlling this sampling here with the watermarking key. And inside that, we have this tournament sampling. And yeah, as I mentioned before, detecting the watermarks requires the secret key and the watermarking functions G1 to Gn. Slide 48 of 52, time stamp 43:54 And again, removing the watermark, because I think that’s maybe interesting to some people, would ideally involve editing all the positions here. But since we don’t know which positions are watermarked and internally, they choose the positions so that they have equally likely tokens at those positions. And there might be positions where that’s not true. So here, for example, for “trees”, we might not even have an alternative word that is high scoring so they don’t watermark that position. So they only do the watermarking at certain positions essentially. Since we don’t know which positions to kind of defeat or remove the watermark, we would... Slide 49 of 52, time stamp 44:29 ...have to edit several places in the text. So what I think that means for the future of AI-generated text is that this actually... Slide 50 of 52, time stamp 44:36 …might result in worse AI-generated text. So I think if there’s a person who likes to use AI-generated text everywhere on the internet, let’s say there’s a news website that likes to use AI-generated text to write the news, I don’t think watermarking will necessarily stop them from doing that. They will probably still want to generate AI-generated text because that’s part of their workflow. So I think my guess is that they’ll use another model. Slide 51 of 52, time stamp 45:08 They’ll just use a second model to edit the text to get the so-called edited AI-generated text. So it’s complicating the pipeline. Instead of getting the text directly from Claude, it’s now using Claude to generate AI-generated text, passing it through a local model, and then having edited AI-generated text, which is likely not watermarked anymore. So why a local model? I just think a local model because I think all the providers- the proprietary LLMs, not only Claude, but also Google— I mean, Google wrote this paper, right? So I’m thinking that they are also watermarking Gemini text. And I think OpenAI is probably already doing it or will do so as well. I mean, I’m just speculating, but I’m imagining everyone will probably do something like that because there’s like an EU regulation that requires that. And that’s, according to the blog post, apparently why Claude is doing it. Yeah, so I’m thinking local models may not, at least not yet, implement this watermarking. So I think people will just use a local model and then generate edited AI-generated text. And my guess is it will be slightly worse than the original text because for the local model, you might now be using a smaller model. So, I mean, you could also technically just use the local model directly to generate text. But in my view, editing text is simpler than generating text. So for the generation of the text, you might use a very expensive high-end, I don’t know, like the highest, most expensive Claude model for complicated text. And then you use a cheaper local model to make these surgical edits, essentially. That’s probably what’s going to happen. And why worse? So if we look back at this graphic where we just added random positions, you might be just changing words for the sake of changing them. And then it risks making the text worse. So you might still have generated text, but it’s kind of like it’s edited awkwardly. Slide 52 of 52, time stamp 47:20 But anyway, so my goal here was to explain how the watermarking works and not, let’s say, the worldwide ramifications of that. But I hope this kind of behind-the-scenes, under-the-hood look is useful. The watermarking is not as complicated as it might seem, but I think it was still 52 slides, so it was also not super trivial. So I hope you found this little lecture useful. And yeah, until next time, see you then. PS: If you like more explainers in this style, I don’t post videos to YouTube regularly, but I have accumulated over 300 videos over the years, which you can find on my YouTube channel here . I also have a YouTube version if you prefer using the YouTube player And here is a link to the slides

0 views
Jeff Geerling 1 weeks ago

Hands-on with Raspberry Pi's CM5 Programming Jig

In the before-times, when Raspberry Pi CM5s were (relatively) affordable, I built a number of Pi clusters ( example ), and one of the most annoying parts of the build was flashing Raspberry Pi OS to all the Pis. One, two, or even three Pis isn't a big deal, but once you hit 4+, the process of plugging the Compute Module into a carrier board, plugging that into a computer, managing Raspberry Pi Imager, and trying to match up details like a hostname, MAC address, and the physical Pi itself, gets annoying.

0 views
Unsung 2 weeks ago

“If nothing happened, you probably did not press the button quickly enough the second time.”

I like learning new things, and I like learning new old things. I was looking at Apple Human Interface Guidelines from 1987 and this passage caught my attention: The most common use of double-clicking is as a shortcut way to perform an action. For example, clicking twice on an icon is a faster way to open it than clicking once to select it, then choosing Open from the File menu; clicking twice on a word to select it is faster than dragging through it. I knew that double click an icon was a shortcut to the first action (typically Open), but I never really thought of double-clicking a word as a faster way to drag across to select it – even though, in hindsight, it makes perfect sense. Another vintage thing I learned of recently from a coworker is this, also covered in the 1987 HIG: If the user begins a double-click sequence, but then drags the mouse between the mouse- down and the mouse-up of the second click, the selection becomes a range of words rather than a single word. This doesn’t feel (to me) like a very pleasant gesture to perform repeatedly, but what feels nice about it is that it automatically snaps the selection to the endings of the words: Part of me would prefer this to be the default behaviour when selecting more than 3 words, or so, so you could be less precise. Anyway. The double clicking to perform default action applies to a lot of lists of things. Here are some examples from Scrivener, Word, and Lightroom – you can double click on each of these items to proceed, without having to select and click the button: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/2.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/3.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/4.1600w.avif" type="image/avif"> But sometimes the creators of such dialogs forget. Here’s Screen Sharing in MacOS, and a notification in Chrome where only the slow path is available – double clicking on items doesn’t do anything: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/5.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/5.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/6.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-nothing-happened-you-probably-did-not-press-the-button-quickly-enough-the-second-time/6.1600w.avif" type="image/avif"> The tricky part about not being a good citizen of a shared user interface is that those omissions aren’t just local to your app – they can ruin the gesture in other places, as people’s fingers learn to distrust it in not just your app, but in general. (The quote in the title is from Apple Macintosh User’s Handbook .) #flow #mouse #text editing

0 views
xenodium 2 weeks ago

Focus windows across Emacs frames

Historically, I've only ever known two great ways of focusing Emacs windows: the built-in command, which cycles through all the available windows, and the ace-window package, offering random window access. It's not that there aren't more or better ways, I just didn't look any further as these were enough for my needs. While I really wanted to make my default choice, for whatever reason, it never stuck. with a custom binding always felt like the smoother fit for my limited needs. You see, I hardly ever have more than two visible windows, so whenever kicked into action (for 3 or more windows), it often took me by surprise. The one area didn't fit my needs revolved around focus requiring visual feedback, but I eventually solved that with winpulse (a little package I wrote). As you can see, I'm a simple man using few Emacs windows, and when it comes to frames, I almost never use more than one. That is until somewhat recently, when I built ytr , a tiny YouTube radio player that sits in the corner of your frame. While it all feels fairly integrated into your frame, under the hood, renders in a separate frame. This broke my trusty flow. I couldn't just focus my radio window using my well-internalized binding. Turns out, actually caters for focusing windows across frames, but only when invoked programmatically. Sure, I can wrap it with my own custom command, but Emacs already had me covered. I found the built-in command. All I had to do was bind it to and Bob's your uncle . I can now switch between my current window and my YouTube radio, with my dear binding. Balance restored.

0 views
Ahead of AI 2 weeks ago

Building an AI Text Detector From Scratch

Substack recently launched its AI detector feature in the UI, which is super interesting. Separately, lots of people asked me about interesting local do-it-yourself LLM projects as demos to show what small language models (SLMs) are capable of. Putting one and one together, I thought it would be interesting to show how an AI detector can be implemented. I will also use it as a verifier to train a small language model to produce text that avoids detection. This is a small educational project for studying the limitations of AI detectors and exploring a verifier-based LLM application beyond regular reasoning models trained on math and code. Figure 1: Substack now features a built-in AI detector. So, as mentioned above, the intended goal of this tutorial is to explain how AI detectors work by building (a simple) one. In practice, such a detector can be used to filter out spammy content, but also to potentially improve your personal writing without turning it into AI-generated text. For example, if you wrote a lengthy article and want to improve spelling and grammar, it is tempting (and actually useful) to use a grammar checker to polish it and improve readability. There are different services for that, including general-purpose LLMs like ChatGPT. However, this also runs the risk that these tools turn your writing, even though it’s still your own writing, into something that is then overpolished and now sounds like AI and gets flagged as spammy content. For example, with an AI checker, one could say, “Fix my grammar while ensuring that my text still scores 0% AI-generated.” Anyway, while we are building a fully functional checker here, the goal is to explain 1) how AI checkers (can) work and 2) use this as a case study for a more general topic on how to build a scorer or verifier that can be used with LLMs. Disclaimer: AI checkers are essentially a cat-and-mouse game. AI checkers may learn to detect a certain pattern that is indicative of AI-generated content. Then, the next LLM may incidentally or deliberately not exhibit that pattern and avoid detection. The AI checker then has to be updated to detect said LLM, and so forth. Plus, it’s also likely to encounter false positives (human written text flagged as AI-generated), but more on that later. There are several goals of this project. The overarching goal is, of course, to illustrate how AI detectors work and show an applied end-to-end LLM project including evaluation, training, and local deployment for real-world use. The outcome of this is an AI-detector API that can be used by humans and agents, and a user-friendly UI. Figure 2: Preview of the local browser interface developed later in this project. It returns a whole-text AI score and can also highlight the scores for individual text chunks. Here, we are going to develop a method similar to Pangram models, which, as far as I know, are behind Substack AI detection feature. I wrote a short article about AI-text detection a while back in 2023: What Are the Different Approaches for Detecting Content Generated by LLMs Such As ChatGPT? And How Do They Work and Differ? In essence, there are different ways to detect AI-written text, from supervised classifiers and perturbation-based probability tests to perplexity measures and watermarking. In this tutorial, we will build a model that returns a 0-100 score. It’s essentially a classifier with an estimated probability score. The probability score will denote how likely a text is AI-generated according to the classifier. (Or, to be precise the score is the classifier’s estimated probability for the AI-generated class based on its training distribution. However, we shouldn’t interpreted it as a general probability that the text was written by AI.) For this, we are going to fine-tune a DistilBERT classifier (similar to what I described in one of my early Substack articles, Finetuning Large Language Models ), but more details on that later when we get to that stage.

0 views
マリウス 2 weeks ago

Recovering BIOS Firmware on the Star Labs StarBook

As I described in my latest quarterly update , a perfectly routine firmware update managed to turn my Star Labs StarBook Mk VI (AMD) into an expensive paperweight. I had simply copy-pasted the one-liner from Star Labs ' official documentation , the script did its thing for about half a minute, shut the device down, and from that point on the StarBook refused to boot. Black screen, keyboard backlight on, the power LED lit, and the speakers occasionally producing a clacking sound. Sadly no amount of the usual turn-it-off-and-on-again rituals or battery disconnects brought it back. The only way out of this situation is to re-flash the BIOS chip externally using an SPI programmer. Star Labs do document this , however their guide assumes you’re using their programming kit together with a dedicated debug board and an FPC cable. That kit is significantly more expensive than a generic programmer, doesn’t list any make or model information, and, at the time of writing, has been permanently out of stock on their web shop. Not exactly helpful when you’re stranded somewhere with a dead laptop that happens to be the only computer you have with you. The good news is that you don’t need any of that, at least for this specific model of the StarBook . As Star Labs ’ own Sean pointed out in the GitHub issue I opened while debugging this mess, this specific StarBook uses a SOIC-8 flash chip, which means you can recover it with a cheap, generic CH341A programmer and an ordinary SPI clip, as long as you respect its voltage. Warning: Flashing a BIOS chip externally can permanently destroy your device if you do it wrong. The flash chip on the AMD StarBook runs at 1.8V and you must use a 1.8V adapter. Driving it at the CH341A ’s default 3.3V risks damaging the chip, and won’t read it correctly anyway. Everything below is what worked for me, documented to the best of my knowledge, but you’re doing this entirely at your own risk. The flash chip on my StarBook Mk VI (AMD) , which I could read off the silicon once I had the backplate off, is a Winbond 25R128JWSQ , a SPI NOR flash in a SOIC-8 package. The suffix on Winbond parts apparently denotes the 1.8V variants. The ubiquitous, three-dollar CH341A “black” programmers that you’ll find on AliExpress , Amazon , and pretty much everywhere else operate their SPI lines at 3.3V (and the parallel header at 5V). Clamp one of those directly onto a 1.8V chip and, best case, reads garbage. Worst case, however, you cook the flash or something downstream of it. The fix is a small 1.8V adapter board (essentially a level shifter with a voltage regulator) that sits between the CH341A and your SOIC-8 clip. These are sold as kits, e.g. the KOOBOOK CH341A Programmer + 1.8V Adapter combo that Sean linked in the issue. Make sure whatever you buy explicitly mentions 1.8V. You will need a CH341A programmer with a 1.8V SOIC-8 adapter, a SOIC-8 test clip (the spring-loaded “Pomona-style” clips, or the cheaper ribbon-cable variety, both work), a second computer running Linux (can be via a live medium, e.g. a USB stick) to drive the programmer from, e.g. a department store laptop and a Fedora live USB will do, if you’re eloquent enough to explain to the staff that you’re definitely not building what almost certainly looks to them like a bomb. You will also need the correct firmware image for your model (more on that below), a small Phillips screwdriver and, ideally, a plastic spudger. Power everything off and unplug the charger before you start. Flip the laptop over and remove the backplate by undoing the two long Phillips screws in the top corners first, and then the eight shorter screws around the edges. Lift the plate off carefully. Then, remove the five screws holding the battery in place (one of the screw positions is intentionally left empty) and gently unplug the battery connector. Last but not least, locate the SOIC-8 flash chip on the mainboard. It’s the little eight-legged Winbond chip described above. Note: While I had the StarBook open, I noticed that my (barely two year old) battery had started to visibly bulge, so do take a moment to inspect yours. A swollen lithium battery is a fire hazard and should be replaced. SOIC-8 flash chips have a defined pin 1, and the clip’s pin 1 (usually the wire on the red edge of the ribbon) has to line up with it. Get the orientation wrong and the chip simply won’t show up. For reference, the pinout of the Winbond SOIC-8 flash is: You don’t have to wire any of this up by hand, though, as the clip and the 1.8V adapter carry all eight lines for you. The only thing you need to get right is aligning pin 1 of the clip with pin 1 of the chip. Note: On my chip there is a gray dot painted onto the package, on the corner opposite to pin 1. Pin 1 is instead marked by the small indented (etched) dot, on the exact opposite side from the painted one. I have no idea why the gray dot is there, but if you align to it you’ll have the clip on backwards. Look for the indentation, not for the gray spot if yours has one too. With the clip attached, plug the CH341A into your second machine. A quick look at should confirm it enumerated: Install if you haven’t already: Before writing anything, make sure can actually talk to the flash over your clip: If everything is seated correctly, will identify the Winbond chip (detected as something like ). If instead you get: …then don’t panic. In my experience this is almost always poor clip contact rather than a real problem. I had to wiggle and reseat the clamp a few times before the chip showed up reliably, because those cheap clips are fiddly. Only proceed once the chip is detected consistently across a couple of runs. Even if the firmware is bricked, it’s good practice to take a backup before you overwrite anything. Read the chip twice and compare the dumps to be sure your contact is solid: If the two reads differ, your clip contact is flaky and you should reseat it and try again. As for the firmware image, Star Labs publish their firmware in a public GitHub repository . For external programming you want a full SPI image, not the EFI/ updater files. For my StarBook Mk VI (AMD) (product SKU ) that’s the image. The full-image files also live under the model’s directory . Pick the one that matches your model and rename it to something convenient, e.g. . Note: Star Labs ’ firmware versioning is, to put it mildly, a mess. As of writing, the last AMI (the original “BIOS”) release for the AMD StarBook is , while onwards is Coreboot . Whichever you decide to flash, just make sure it’s a full image for your exact model. Last but not least, write the downloaded image using the command: By default will erase, write, and then verify the chip. Star Labs ' official command appends (i.e. and ) to skip those verification passes, but I’d recommend leaving them off so confirms the write actually stuck. Either way, do not disconnect or disturb the programmer while it’s working. Once it finishes successfully, remove the clip, reconnect the battery, screw the backplate back on, and try to boot. When I powered mine back on, the StarBook came to life again, only to stop at a screen complaining about a missing boot entry, since flashing a fresh image also wipes the EFI boot variables. That’s nothing dramatic and you just need to point the firmware back at your bootloader. You can either use the boot menu and pick your SSD, which usually re-adds the boot entry, or boot a recovery/live system and run (this is what I did), or drop into the EFI shell and launch your bootloader manually: Note: On newer Coreboot releases Star Labs are enabling Rom Armor and anti-rollback. On the AMD board external flashing and downgrading still worked for me on , but this is expected to be locked down from onwards. What frustrates me most about this whole ordeal isn’t that a firmware update can go wrong, because that’s always a risk when you flash something. It’s that Star Labs ’ documented recovery path depends on a proprietary kit that nobody can actually buy, when a generic CH341A with a 1.8V adapter seemingly does the job just fine. However, this info is nowhere to be found in Star Labs ’ official documentation, which is why I decided to publish this write-up to begin with. Hopefully it spares the next person the day (and the stress) it cost me.

0 views
Nelson Figueroa 4 weeks ago

Setting Up a Time Machine Drive from the Command Line

It’s possible to set up Time Machine drives from the command line. This is way more convenient and becomes scriptable (there’s one GUI checkbox at the end if you encrypt, so the drive can unlock itself). Also, based on my own personal experience, the Time Machine GUI can be unresponsive so the CLI is much better. I’ll be using a 1TB external SSD in this guide. Here’s how you set up a drive. Some of these terminal commands require Full Disk Access. Specifically, the commands we’ll be using later on. Grant your preferred terminal app full disk access in System Settings -> Privacy & Security -> Full Disk Access. Plug in your drive. Unlock it if you have to. Run the following to get some information we’ll need. Here is what you’ll see if the drive is currently being used for Time Machine: Here is what you’ll see if the drive is brand new: Two identifiers matter here: Confirm that you have the correct disk with this command. We know this one is the external drive due to the line. On Apple silicon the internal drive shows and . We’ll need to erase the disk next. The steps vary slightly depending on whether the drive is brand new or is an existing Time Machine drive. New drives usually ship as ExFAT with an MBR partition scheme, so there’s no APFS container yet. We can erase and convert the disk with this command: This results in a drive with a GPT scheme, an EFI partition, and an APFS container with one volume in it (read more on containers vs volumes in APFS: Containers and Volumes ). It does not encrypt the volume, enable ownership, or set the Time Machine role, which are all things we need. So we’ll delete this newly created volume and create a proper one later. Run this again to figure out the identifier: The identifier is in this case. Now use that identifier to delete the volume that was created in the step: That’s it for this section. Skip ahead to the “Create the Volume” section. If the drive is already being used for Time Machine we need to remove the destination (the disk entry in the Time Machine GUI). First, figure out the destination UUID: Then remove the old destination. We’re using so it’ll prompt you for your machine’s password. Verify it’s gone: You can double check that this worked by checking in System Settings -> General -> Time Machine. There should be no backup drive listed. If it’s still there, you can manually remove it in the GUI. Now delete the old volume. The container stays, so there’s no need to repartition the whole disk: If you get an error like: That’s Spotlight. Removing the Time Machine destination makes macOS stop treating the drive as a backup target, so Spotlight starts indexing it like any other volume and holds it open. Turn indexing off for that volume and try again: Now that the external drive has been erased, we need to create an APFS volume on the drive. Decide if you want your backups to be unencrypted or encrypted and follow the corresponding steps. Note that this only worked for me with the flag in the commands. Do not leave it out! If you skip this option, macOS deletes the volume you just created and builds its own in its place when you register the drive in a later step. It has to do with APFS volume roles. The role is for Time Machine backup stores. You can read more about these roles here: How do APFS volume roles work? . Run the following command to create a volume without encryption: Run the following command. It’ll prompt you for a password for your drive. Run this to confirm everything went well. Under you’ll see if it’s an encrypted volume. You’ll see if it’s an unencrypted volume. Two things to note here: Time Machine refuses any destination that doesn’t enforce file ownership. It can’t preserve the UID or GID of what it backs up without file ownership. Volumes created from the command line have it turned off by default. Run the following to enable ownership, replacing the path with your own external drive’s path: Run the following to double check that it worked. should be : “Registering” means telling Time Machine to use this volume as a backup destination. It’s what the GUI’s “Add Backup Disk” button does. The button and the command we’re going to run both write to . Run the following to register your drive: No output means it worked. appends to your destination list rather than replacing it. If you run into this error, try waiting a bit and then try again: Then run these commands to double check everything went well: The output containing confirms that a destination exists and its volume is reachable. Confirm it points at your volume and not a replacement: That UUID should match the one from earlier. If it doesn’t, macOS replaced your volume with one of its own, which is what happens when the flag gets left out. We can add paths we want to exclude from backups through the command line too. There are three kinds of exclusions: fixed-path exclusions, sticky exclusions, and volume exclusions. But for our purposes we only care about fixed-path and sticky exclusions. Here’s an example of adding a fixed-path exclusion: Here’s an example of adding a sticky exclusion (same command without the this time): Check any path to make sure it was added to the exclusions. You should see next to the path (If you see that means no file or directory is there, not that the exclusion didn’t register): To list fixed-path exclusions we need to read them out of the preferences plist: Sticky exclusions don’t appear in the preferences and are stored as an extended attribute on the item: Removing them is similar to adding. We use instead. The flag is still necessary for removing fixed-path exclusions but not for sticky exclusions. Here’s an example of how to remove a fixed-path exclusion: And here’s an example of how to remove a sticky exclusion: Try manually starting a backup through the command line: The command above may look like it’s stuck if your backup takes a while. You can run this in a separate terminal tab/window to monitor its progress: If you get an error like: That just means macOS started a backup automatically. Once it’s done you can verify with: The path in the output confirms that a real backup exists. You can also check the result code: means the backup was successful. Anything else means the last backup failed. This only applies to encrypted drives. From what I can tell, there’s no way to store the Time Machine drive’s passphrase in Apple Keychain using the command line. If you prefer your drive to unlock automatically when it’s plugged into your machine, you’ll need to do the following. Eject the drive (change the path name to your drive’s): Plug it back in. When the password dialog appears, type the passphrase and check “Remember this password.” That’ll save the passphrase in your local keychain so that macOS can unlock the drive automatically next time you plug it in. No need to type in the passphrase every time. Confirm it worked by ejecting and replugging once more. If you aren’t prompted to type in your passphrase, that means it worked. You can double check via the command line too: If is present, that means the drive unlocked and mounted. If you go through this process a few times there’s a good chance you’ll have several Keychain entries for old Time Machine drives. Deleting a volume doesn’t remove its Keychain entry, you’ll have to do this manually. Normally, these Keychain entries point at volumes. But since those volumes were deleted, the entries are pointing at volumes that no longer exist. We can run the following to list all relevant Keychain entries: There’s two entries. To find the one that actually points to a volume, run: So UUID points to a volume. Which means UUID is safe to delete. We can delete it like so: We can then double check that we only have the necessary Keychain entries left: However, this is just for the sake of being tidy. I don’t think having these kinds of entries in Keychain affects macOS negatively in a significant way. — the whole physical disk. We’ll need this later on when running the command. — the APFS container. This is what we’ll need for the command. (A brand new drive won’t have this one yet. It’ll get created when we erase the disk in a later step.) The volume identifier won’t always be , APFS reuses freed slots so yours may be something like or . Write down the volume UUID, we’ll need it later on to verify everything works. Fixed-path exclusions are tied to a path regardless of what is there. Use these exclusions for anything that gets deleted and recreated, like build caches. Sticky exclusions are the default. They’re tied to the item itself. It follows the file if you move it and copies inherit it. Deleting and recreating a directory loses its stickiness. https://support.apple.com/guide/mac-help/back-up-your-mac-with-time-machine-mh35860/mac https://keith.github.io/xcode-man-pages/diskutil.8.html https://keith.github.io/xcode-man-pages/tmutil.8.html https://eclecticlight.co/2024/11/21/how-do-apfs-volume-roles-work/ https://eclecticlight.co/2024/04/02/apfs-containers-and-volumes/ https://eclecticlight.co/2021/10/12/juggling-with-hfs-and-apfs-partitions-and-volumes-a-primer/

0 views

New Declarative Website Menu with Invoker Commands and noscript Hacks!

Read on the website: I updated my website menu to be prettier on mobile, and I did not sacrifice accessibility and noJS folks! Go check it out and adopt it!

0 views
Jeff Geerling 1 months ago

Getting 25 Gbps Thunderbolt Ethernet on my Mac Studio

I've been using the built-in 10 Gigabit Ethernet on my Mac Studio for a few years. It works fine: I can edit 4K video straight off my NAS over the network, and run backups at around 1 GB/sec. But... I want more . I upgraded my rack and my NAS to 25 GbE a couple years ago, and wanted to upgrade my main workstation, too.

0 views
Michael Hoffmann 1 months ago

How to Set Up an MCP Server for an Existing Nuxt App

A beginner-friendly, practical guide to adding an MCP server to an existing Nuxt app using the Nuxt MCP Toolkit and a mocked weather tool.

0 views
dfir.ch 1 months ago

Field Notes: NSSM - the Non-Sucking Service Manager

Introduction While analyzing Autoruns entries during a Compromise Assessment or an Incident Response case, would you take a second look at the nssm.exe binary running as a service? Hopefully. In a recent case, attackers used nssm.exe to start ngrok as a service to maintain a persistent backdoor. Here is a practical breakdown of how this mechanism works and how you can hunt for it in your environment. Figure 1: NSSM service Why Attackers Love NSSM NSSM (the Non-Sucking Service Manager) is a legitimate tool designed to run standard executables as Windows services.

0 views
Gregory Gundersen 1 months ago

What Is the Inverse Covariance Matrix?

In statistics and optimization, we often multiply a vector x \mathbf{x} x by the inverse of its covariance matrix Σ \boldsymbol{\Sigma} Σ : Σ − 1 x . (1) \boldsymbol{\Sigma}^{-1} \mathbf{x}. \tag{1} Σ − 1 x . ( 1 ) For example, you see this in the probability density function of the multivariate normal distribution or in the analytic solution to mean–variance optimization. What does this operation do? I’ve written about this before, when I discussed the covariance matrix Σ \boldsymbol{\Sigma} Σ as high-dimensional variance , but that was cursory at best. The goal of this post is to explore this operation in more detail. If Σ \boldsymbol{\Sigma} Σ is a 1 × 1 1 \times 1 1 × 1 matrix, then clearly x \mathbf{x} x is a 1 1 1 -vector, and this just normalizes the scalar value: x σ 2 . (2) \frac{x}{\sigma^2}. \tag{2} σ 2 x ​ . ( 2 ) And if Σ \boldsymbol{\Sigma} Σ is a diagonal P × P P \times P P × P matrix, then clearly x \mathbf{x} x is a P P P -vector, and this just normalizes each component of x \mathbf{x} x : Σ − 1 x = [ x 1 / σ 1 2 x 2 / σ 2 2 ⋮ x P / σ P 2 ] . (3) \boldsymbol{\Sigma}^{-1} \mathbf{x} = \begin{bmatrix} x_1 / \sigma_1^2 \\ x_2 / \sigma_2^2 \\ \vdots \\ x_P / \sigma_P^2 \end{bmatrix}. \tag{3} Σ − 1 x = ⎣ ⎢ ⎢ ⎢ ⎢ ⎡ ​ x 1 ​ / σ 1 2 ​ x 2 ​ / σ 2 2 ​ ⋮ x P ​ / σ P 2 ​ ​ ⎦ ⎥ ⎥ ⎥ ⎥ ⎤ ​ . ( 3 ) The interesting case is when Σ \boldsymbol{\Sigma} Σ is a non-diagonal P × P P \times P P × P matrix. In that case, we can visualize the operation via the eigendecomposition, sometimes called the spectral decomposition . Recall that the eigendecomposition of a square matrix is Σ = Q Λ Q ⊤ , (4) \boldsymbol{\Sigma} = \mathbf{Q} \boldsymbol{\Lambda} \mathbf{Q}^{\top}, \tag{4} Σ = Q Λ Q ⊤ , ( 4 ) where Q \mathbf{Q} Q is a square matrix whose n n n -th column is the n n n -th eigenvector of Σ \boldsymbol{\Sigma} Σ and where Λ \boldsymbol{\Lambda} Λ is a diagonal matrix of associated eigenvalues { λ 1 , … , λ P } \{\lambda_1, \dots, \lambda_P \} { λ 1 ​ , … , λ P ​ } . Since Q \mathbf{Q} Q is an orthonormal basis and Λ \boldsymbol{\Lambda} Λ is a diagonal matrix, we can think of it as: rotate our data into a vector space in which the transformed data is orthogonal, scale in this space, and rotate back to the original vector space. If this kind of geometric interpretation makes no sense, please see my post on the singular value decomposition (SVD). Now let’s see this in equations. Raising this matrix to a power can be simplified as: Σ n = ( Q Λ Q ⊤ ) ( Q Λ Q ⊤ ) … ( Q Λ Q ⊤ ) ⏟ k  times = Q Λ Λ … Λ ⏟ k  times Q ⊤ = Q Λ k Q ⊤ . (5) \boldsymbol{\Sigma}^n = \underbrace{\left( \mathbf{Q} \boldsymbol{\Lambda} \mathbf{Q}^{\top} \right) \left( \mathbf{Q} \boldsymbol{\Lambda} \mathbf{Q}^{\top} \right) \dots \left( \mathbf{Q} \boldsymbol{\Lambda} \mathbf{Q}^{\top} \right)}_{\text{$k$ times}} = \mathbf{Q} \underbrace{\boldsymbol{\Lambda} \boldsymbol{\Lambda} \dots \boldsymbol{\Lambda}}_{\text{$k$ times}} \mathbf{Q}^{\top} = \mathbf{Q} \boldsymbol{\Lambda}^k \mathbf{Q}^{\top}. \tag{5} Σ n = k  times ( Q Λ Q ⊤ ) ( Q Λ Q ⊤ ) … ( Q Λ Q ⊤ ) ​ ​ = Q k  times Λ Λ … Λ ​ ​ Q ⊤ = Q Λ k Q ⊤ . ( 5 ) This works because Q \mathbf{Q} Q is orthonormal and thus Q Q ⊤ = Q ⊤ Q = I , (6) \mathbf{Q} \mathbf{Q}^{\top} = \mathbf{Q}^{\top} \mathbf{Q} = \mathbf{I}, \tag{6} Q Q ⊤ = Q ⊤ Q = I , ( 6 ) where I \mathbf{I} I is the identity matrix. And this means that Λ k \boldsymbol{\Lambda}^k Λ k is just a diagonal matrix where each element is raised to the power k k k . It follows that the inverse matrix Σ − 1 \boldsymbol{\Sigma}^{-1} Σ − 1 is the original spectral decomposition but with Λ − 1 \boldsymbol{\Lambda}^{-1} Λ − 1 instead of Λ \boldsymbol{\Lambda} Λ . And we can interpret multiplying a vector x \mathbf{x} x by this inverse as follows: z : = Q ⊤ x rotate into orthogonal vector space z ~ : = Λ − 1 z scale by the eigenvalues x ~ : = Q z ~ rotate back into original vector space (7) \begin{aligned} \mathbf{z} &:= \mathbf{Q}^{\top} \mathbf{x} && \text{rotate into orthogonal vector space} \\ \tilde{\mathbf{z}} &:= \boldsymbol{\Lambda}^{-1} \mathbf{z} && \text{scale by the eigenvalues} \\ \tilde{\mathbf{x}} &:= \mathbf{Q} \tilde{\mathbf{z}} && \text{rotate back into original vector space} \end{aligned} \tag{7} z z ~ x ~ ​ : = Q ⊤ x : = Λ − 1 z : = Q z ~ ​ ​ rotate into orthogonal vector space scale by the eigenvalues rotate back into original vector space ​ ( 7 ) What will this operation actually do? We can visualize it in Figure 1 1 1 . In my mind, the geometric fact that jumps out the most is that when we divide by eigenvalues, we convert our ellipsoid into another ellipsoid, stretching along the dimension that originally was the smaller of the two. So for example, if Σ \boldsymbol{\Sigma} Σ was the empirical covariance matrix of x \mathbf{x} x , we would normalize a la Equation 2 2 2 but in P P P dimensions. Variables of x \mathbf{x} x with high variance get downweighted. This might be surprising at first. Perhaps you expected this operation to “whiten” the data in the sense of being the multivariate analog to dividing by the standard deviation. But it makes sense that it is not, because as we saw above, if Σ \boldsymbol{\Sigma} Σ is a digaonal matrix, then Σ − 1 \boldsymbol{\Sigma}^{-1} Σ − 1 is like dividing by variances, not standard deviations. This suggests that to whiten the data, we simply need to multiply x \mathbf{x} x by Σ \boldsymbol{\Sigma} Σ raised to the power k = − 1 / 2 k = -1/2 k = − 1 / 2 . See Figure 2 2 2 . We can see that after the initial rotation, dividing by the square root of the eigenvalues converts our ellipsoid into a sphere. The final rotation simply rotates the whitened data back into “data space”. This should make sense if you understand the SVD and that the singular values are the square roots of the eigenvalues. To see this, think about the SVD of our data matrix: X = U S V ⊤ . (8) \mathbf{X} = \mathbf{U S V}^{\top}. \tag{8} X = U S V ⊤ . ( 8 ) Here, X \mathbf{X} X has shape N × P N \times P N × P , where N N N is the number of observations and P P P is again the dimensionality of our data. Intuitively, the singular values of a matrix X \mathbf{X} X are in the units of X \mathbf{X} X , since U \mathbf{U} U and V \mathbf{V} V are just rotations. And what are the eigenvalues? They’re the square of the singular values, since: Σ : = X ⊤ X = ( U S V ⊤ ) ⊤ ( U S V ⊤ ) = V S 2 V ⊤ . (9) \boldsymbol{\Sigma} := \mathbf{X}^{\top} \mathbf{X} = (\mathbf{U S V}^{\top})^{\top} (\mathbf{U S V}^{\top}) = \mathbf{V S}^2 \mathbf{V}^{\top}. \tag{9} Σ : = X ⊤ X = ( U S V ⊤ ) ⊤ ( U S V ⊤ ) = V S 2 V ⊤ . ( 9 ) Geometrically, we stretch by X ⊤ \mathbf{X}^{\top} X ⊤ and then by X \mathbf{X} X —so we’re double stretching. See Suraj Rampure’s notes for a bit more detail. But the high-level point is that Σ − 1 x \boldsymbol{\Sigma}^{-1} \mathbf{x} Σ − 1 x is the multivariate analog of normalizing by the variance, while Σ − 1 / 2 x \boldsymbol{\Sigma}^{-1/2} \mathbf{x} Σ − 1 / 2 x is multivariate analog of normalizing by the standard deviation. Finally, this means we can generalize to any power of k k k of Σ \boldsymbol{\Sigma} Σ . When k k k is large, our data is stretched along it’s current direction. When k k k is negative, the data is stretched in the opposite direciton. And when k ≈ 0 k \approx 0 k ≈ 0 , then the data is unchanged (Figure 3 3 3 ). This view of the inverse covariance matrix is helpful in interpreting other operations. For example, consider this derivation, with hopefully obvious notation, where now u \mathbf{u} u and v \mathbf{v} v denote arbitrary vectors unrelated to the SVD: u ⊤ D − 1 v = u ⊤ Q Λ − 1 Q ⊤ v = u Q ⊤ Λ − 1 v Q = u Q ⊤ Λ − 1 / 2 Λ − 1 / 2 v Q = u Q Λ ⊤ v Q Λ . (10) \begin{aligned} \mathbf{u}^{\top} \mathbf{D}^{-1} \mathbf{v} &= \mathbf{u}^{\top} \mathbf{Q} \boldsymbol{\Lambda}^{-1} \mathbf{Q}^{\top} \mathbf{v} \\ &= \mathbf{u}_Q^{\top} \boldsymbol{\Lambda}^{-1} \mathbf{v}_Q \\ &= \mathbf{u}_Q^{\top} \boldsymbol{\Lambda}^{-1/2} \boldsymbol{\Lambda}^{-1/2} \mathbf{v}_Q \\ &= \mathbf{u}_{Q\Lambda}^{\top} \mathbf{v}_{Q\Lambda}. \end{aligned} \tag{10} u ⊤ D − 1 v ​ = u ⊤ Q Λ − 1 Q ⊤ v = u Q ⊤ ​ Λ − 1 v Q ​ = u Q ⊤ ​ Λ − 1 / 2 Λ − 1 / 2 v Q ​ = u Q Λ ⊤ ​ v Q Λ ​ . ​ ( 1 0 ) This shows that u ⊤ D − 1 v \mathbf{u}^{\top} \mathbf{D}^{-1} \mathbf{v} u ⊤ D − 1 v is just a standard Euclidean inner product u Q Λ ⊤ v Q Λ \mathbf{u}_{Q\Lambda}^{\top} \mathbf{v}_{Q\Lambda} u Q Λ ⊤ ​ v Q Λ ​ after we rotate and whiten both vectors. The inverse matrix D − 1 \mathbf{D}^{-1} D − 1 tells us how to transform each vector into a space where the standard dot product is the correct notion of distance. For a more complicated example, consider a common modeling assumption, which is that a variable x n ∈ R P \mathbf{x}_n \in \mathbb{R}^{P} x n ​ ∈ R P is a linear function of a low-dimensional factor f n ∈ R K \mathbf{f}_n \in \mathbb{R}^K f n ​ ∈ R K where K ≪ P K \ll P K ≪ P . We can write this in matrix notation as X = F B + E , (11) \mathbf{X} = \mathbf{F} \mathbf{B} + \mathbf{E}, \tag{11} X = F B + E , ( 1 1 ) where X ∈ R N × P \mathbf{X} \in \mathbb{R}^{N \times P} X ∈ R N × P , F ∈ R N × K \mathbf{F} \in \mathbb{R}^{N \times K} F ∈ R N × K , B ∈ R K × P \mathbf{B} \in \mathbb{R}^{K \times P} B ∈ R K × P , and E ∈ R N × P \mathbf{E} \in \mathbb{R}^{N \times P} E ∈ R N × P . Under some common assumptions, we can show that the covariance matrix of x \mathbf{x} x is Σ x : = cov [ x , x ] = B ⊤ Σ f B + Σ e . (12) \boldsymbol{\Sigma}_x := \text{cov}[\mathbf{x}, \mathbf{x}] = \mathbf{B}^{\top} \boldsymbol{\Sigma}_f \mathbf{B} + \boldsymbol{\Sigma}_{e}. \tag{12} Σ x ​ : = cov [ x , x ] = B ⊤ Σ f ​ B + Σ e ​ . ( 1 2 ) For more details, we see factor analysis . In this scenario, the Woodbury identity tells us how to invert our low-rank covariance matrix Σ x \boldsymbol{\Sigma}_x Σ x ​ : Σ x − 1 = Σ e − 1 − Σ e − 1 B ⊤ ( Σ f − 1 + B Σ e − 1 B ⊤ ) − 1 B Σ e − 1 . (13) \boldsymbol{\Sigma}_x^{-1} = \boldsymbol{\Sigma}_{e}^{-1} - \boldsymbol{\Sigma}_{e}^{-1} \mathbf{B}^{\top} \left(\boldsymbol{\Sigma}_f^{-1} + \mathbf{B} \boldsymbol{\Sigma}_{e}^{-1} \mathbf{B}^{\top} \right)^{-1} \mathbf{B} \boldsymbol{\Sigma}_{e}^{-1}. \tag{13} Σ x − 1 ​ = Σ e − 1 ​ − Σ e − 1 ​ B ⊤ ( Σ f − 1 ​ + B Σ e − 1 ​ B ⊤ ) − 1 B Σ e − 1 ​ . ( 1 3 ) Now here’s the question: what does it mean to normalize an observation x n \mathbf{x}_n x n ​ by this quantity? To understand, let’s rewrite this as Σ x − 1 x n = Σ e − 1 x n − Σ e − 1 B ⊤ ( Σ f − 1 + B Σ e − 1 B ⊤ ) − 1 B Σ e − 1 x n = Σ e − 1 ( x n − B n ⊤ f ^ n ) = Σ e − 1 x ~ n , (14) \begin{aligned} \boldsymbol{\Sigma}_x^{-1} \mathbf{x}_n &= \boldsymbol{\Sigma}_{e}^{-1} \mathbf{x}_n - \boldsymbol{\Sigma}_{e}^{-1} \mathbf{B}^{\top} \left(\boldsymbol{\Sigma}_f^{-1} + \mathbf{B} \boldsymbol{\Sigma}_{e}^{-1} \mathbf{B}^{\top} \right)^{-1} \mathbf{B} \boldsymbol{\Sigma}_{e}^{-1} \mathbf{x}_n \\ &= \boldsymbol{\Sigma}_{e}^{-1} \left( \mathbf{x}_n - \mathbf{B}^{\top}_n \hat{\mathbf{f}}_n \right) \\ &= \boldsymbol{\Sigma}_{e}^{-1} \tilde{\mathbf{x}}_n, \end{aligned} \tag{14} Σ x − 1 ​ x n ​ ​ = Σ e − 1 ​ x n ​ − Σ e − 1 ​ B ⊤ ( Σ f − 1 ​ + B Σ e − 1 ​ B ⊤ ) − 1 B Σ e − 1 ​ x n ​ = Σ e − 1 ​ ( x n ​ − B n ⊤ ​ f ^ n ​ ) = Σ e − 1 ​ x ~ n ​ , ​ ( 1 4 ) where f ^ n \hat{\mathbf{f}}_n f ^ n ​ is the generalized least squares estimator of the factor f n \mathbf{f}_n f n ​ using weights Σ e \boldsymbol{\Sigma}_e Σ e ​ and Ridge penality Σ f \boldsymbol{\Sigma}_f Σ f ​ , f ^ n : = ( Σ f − 1 + B Σ e − 1 B ⊤ ) − 1 B Σ e − 1 x n , (15) \hat{\mathbf{f}}_n := \left(\boldsymbol{\Sigma}_f^{-1} + \mathbf{B} \boldsymbol{\Sigma}_{e}^{-1} \mathbf{B}^{\top} \right)^{-1} \mathbf{B} \boldsymbol{\Sigma}_{e}^{-1} \mathbf{x}_n, \tag{15} f ^ n ​ : = ( Σ f − 1 ​ + B Σ e − 1 ​ B ⊤ ) − 1 B Σ e − 1 ​ x n ​ , ( 1 5 ) and where x ~ n \tilde{\mathbf{x}}_n x ~ n ​ is thus our observation residualized by its estimated factor. This is why, if you squint your eyes, parts of the Woodbury identity look like the normal equation for ordinary least squares . It’s a projection. In words, we can say: we estimate the factor component of x n \mathbf{x}_n x n ​ , subtract that factor component out, and then whiten the residual against the idiosyncratic variances.

0 views
Unsung 1 months ago

A time machine in Logic Pro

A nice moment in Logic Pro, a music app. Like with most such apps, you can press R to record you playing an instrument. However, if you forgot to play record, or were just goofing around and stumbled upon something wonderful, you can press ⇧R and the recording will appear anyway, as if you had a time machine. This is a quick TikTok video showing it in action: = 3x)" srcset="https://unsung.aresluna.org/_media/a-time-machine-in-logic-pro/tt1-play.1600w.avif" type="image/avif"> The feature is called Flashback Capture. Of course, just like with undo send , this is no magic. The app is always recording the events quietly, and then offers you to make them “real” if you want. I dug around and found a support document that offers a rare view into the mechanics of this feature, which are slightly more sophisticated than I imagined: When playback is stopped, Flashback Capture creates a separate region containing all the MIDI events received since the last playback. However, after a pause of 20 seconds between incoming MIDI events, those initial MIDI events before the pause are discarded. If when playback is stopped, you perform some MIDI events and then pause for 1.5 bars or longer, those initial notes aren’t included in the visible part of your region. If you do want those MIDI events to be included in the created region, you can drag the left region boundary to expose them. What it seems to say is: This seems like a good and thoughtful feature that prevents data loss, a sort of magical “reverse redo.” (Thank you to Chris Krycho for telling me about this feature, which is apparently also available in other music apps, e.g. Cubase and Dorico.) #above and beyond #details #errors #preview #undo If there is a longer pause within your play, the notes are still recovered, but hidden. You can always drag to reveal them, but in effect, only the most recent of your notes are immediately visible – a nice touch. (The moment you start dragging, it also shows you a quick preview of where you’re going, which is also thoughtful!) If the pause is 20 seconds or more, all the notes before that pause are no longer preserved – presumably to prevent too much wasted data in your file. However, after you stop playing, you have infinite time to invoke Flashback Capture and recover the notes.

0 views
tekin.co.uk 1 months ago

10 Things You Might Not Know About Rails i18n

This is mostly a transcript of my recent lightning talk on i18n in Rails , go watch that if you prefer your content in video form. Otherwise read on to find out why I believe understanding i18n in Rails can be useful to you, a Rails developer, even if you’re not translating your applications into other languages. I’m going to share ten things I think are interesting in i18n, and explain how we can take advantage of them in our single language app code. But first a brief primer on i18n… i18n is shorthand for Internationalisation When we’re talking about i18n, it’s useful to understand the difference between Internationalisation and Localisation : The take away here is that although i18n is primarily a framework for adapting applications to support other languages, the core idea of separating content from applications code has useful properties that we can take advantage of it to improve our Rails applications, even if they only support a single language. Rails ships with the i18n gem , and there are two main components: Rails aliases these helpers in views and helper modules to the convenient shorthands of and respectively. So the most basic usage of i18n would look something like this: Primer over! On to the ten things… Even if you’re not localising your app you’re almost certainly still using i18n. That’s because it’s baked into the very fabric of the framework. For example Active Record uses i18n under the hood to generate error messages for its validations. It does so using a YAML file that ship with Active Model : Whenever you see something like “Title cannot be blank”, this is the i18n plumbing that was used to construct it. Rails i18n makes it easy to override these default errors messages. Just replicate the same key structure in your application’s locale file with your chosen phrasing: Rails will check your application’s locale file first, falling back to the defaults only if the message isn’t defined there. As well as overriding this global phrasing, it’s also possible to override the error messages for a given attribute name: Or you can get even more granular and override specific attributes on a given model. For example if we have the following model and validation: It’s possible to set a bespoke error message for this specific model and attribute: As well as overriding individual validation messages globally and for specific attributes, it’s also possible to change the actual format of error messages. By default, errors are constructed with the familiar format of . This is defined in the same YAML that ships with Active Model alongside the default error messages: This format works as a reasonable default to ship with the framework as it gives us mostly coherent error messages out of the box without us having to do anything, but it lacks fidelity and can result in some clunky and unfriendly sounding messages. For example, take this validation: If we go with the default format we end up with . Which is fine, but we can definitely do better. My personal preference on the apps I work with is to remove the attribute prefix entirely: This frees me from having to make my error messages work with attribute’s name upfront, allowing me to write more user-friendly and coherent error messages: Now changing the default format does come at a cost: without the attribute name prefix, the default messages that ship with Rails no longer form complete sentences. This forces me to write bespoke error messages for each validation I add to my app: Personally I’m happy to pay this cost for the benefit of improving the user experience. So whilst you may not want to do this on an existing app with thousands of error messages to backfill, it might be something you consider next time you run and are starting from a clean slate. Update : There’s actually a fairly neat way to avoid this cost, check out my follow-up post on how . As well using i18n to manage and override the error messages for the built-in validations, you can also use it to manage the messages for your custom validations. Let’s look at a custom model validation: Here the error message is inlined as part of the validation code. Instead of doing this, we can pass in a symbol identifying the error, and define the corresponding message in the locale file using the same key structure as the built-in validations: This results in more concise and compact model code, but also equally as nice is that it removes a presentational concern — the specific phrasing of the message — our the business logic. It’s also possible to interpolate both the attribute name and the value of the attribute directly into the error message, which is useful for custom validations that are used across multiple attributes and/or models: Another place where Rails makes use of i18n under the hood is the form helpers: specifically, the label helper methods. By default the label helper methods humanize the attribute name to arrive at the label text: One way to override the label text is to pass in a string to the helper call like so: I think a better way is to define label overrides in the application’s locale file: Not only does this keep the view code more concise and less noisy, but it has the added advantage of defining this label override in one place: now any other forms that display the same label will automatically get the overridden text without you having to manually copy the literal string to every template. And as with form errors, label overrides can be defined per-model as well as globally: i18n has sophisticated support for pluralisation that we can leverage to simplify our app code. As an example, we can display the stock availability of a product by calling out to i18n like so: And then define the phrasing based on the number of items in stock in our locale file: Much neater than a bunch of conditional logic in a helper method! As well as being available in Rails controllers and views, i18n is also integrated into Action Mailer, allowing you to organise content for mailer templates in local files. Mailer get one extra feature of controllers: the ability to specify email subject lines in locale files: Again, this enables us to move another presentational concern out of our application code and put it somewhere more appropriate. There is also a meta benefit to defining all these bits of content (mailer subject lines, form labels, validation error messages) in an application’s locale file: it puts them all in one centralised place, making them easier to change, check for consistency and generally manage. It also makes the content more accessible to non-developers such as designers and product folks. Right at the top I mentioned the method that is used to output localised times and dates. The way this works is by defining our own time and date formats in our locale file using the same format specification as Ruby’s : Here we’ve defined a short date format that will be familiar to my UK-based audience: day/month/year. To render a date using this format we simply pass in the format identifier to (or its alias): or with the shorthand: Now my American readers may be confused by this short format, as across the pond the preferred way to display dates is actually month/day/year. So whilst you may not need to localise your entire application, you might want to consider localising your date rendering, especially if you have customers both sides of the Atlantic. To do this you would define a US region specific English locale file: Then update your controller code to automatically switch locale so the user gets dates formatted appropriately for them. There are many ways to do actually do the locale switching, but at a high-level, you identify the user’s specific locale and then make sure your controller actions are executed with that locale. Here’s an example of doing that by sniffing the user’s locale from their browser headers: (The exact mechanics of the method is left as a separate exercise, but you could do worse than use the http_accept_language library) Little known fact: the i18n library supports locale files defined using Ruby! Now why would you do this, other than a burning hatred for YAML? Well one reason is that Ruby-based locale files allows you to make use of procs to dynamically generate content. Below is an excerpt from a Ruby-based locale file from one of my applications. It uses a proc to format dates as academic years, changing the output depending on which side of September the date is: Now this could just as easily be achieved using a standard Rails helper method. But for me I like the consistency and clarity of using a single mechanism ( ) for formatting dates across my application code, rather than with a mishmash of calls to bespoke helpers, , , etc. This is the part of the lightning talk where I make a meta point about what I’ve covered. I won’t write that up here, but if you’re interested go watch the video from minute 9. Internationalisation — the process of abstracting content and other locale-specific things away from application code itself Localisation — the process of adapting software to support different languages and regions (maybe using i18n) locale files where textual content is organised and stored per-language (normally as YAML) two helper methods: for outputting text content from the locale files for localising dates and times

0 views