I’ve been experimenting with making a short vertical reel to go with these posts. Not every one of them, and not reliably… the experiment has run away with itself a bit, which is roughly what curiosity does when there’s nobody about to stop it.
The intention is that most posts eventually carry some form of reel I can put on social media. The interesting ones, at any rate. The boringly technical ones can probably manage without.
The reason to bother at all is reach: TikTok, Reels and Shorts will put a thing in front of people who have never heard of you, which a blog post on its own simply will not do. And making one by hand takes an evening, which is fine once and completely impossible as a habit. Hence keryx.
This walkthrough is the cheap end of it, and deliberately so: no narration, no music, no generated card art. No API keys, and nothing to pay for. You write a three-card storyboard, you run one command, and you get a real 1080×1920 H.264 mp4 on disk. Fifteen minutes.
That matters more than it sounds, because every other path through keryx eventually reaches for a provider that bills you, and I’d rather you found out whether the tool does what you expect before you point money at it. Voice, music and generated art come later, and the how-to guides cover them one at a time.
I should say the tool has grown a good deal past anything this blog strictly needed. That’s curiosity for you. But it does mean the free path is a real one rather than a crippled demo, and it’s genuinely where I’d start again.
This is the canonical walkthrough from the keryx docs, reproduced here so the two don’t drift: https://keryx.phpboyscout.uk/tutorials/first-reel/. Every command and output below was validated by running it against v0.11.0. keryx has moved on since: it now renders with WebAssembly by default rather than shelling out, which the prerequisites and the last section reflect. The console output is still what v0.11.0 printed.
Before you start
- Go 1.26 or newer, if you’re installing from source.
- No video software. keryx renders with FFmpeg compiled to WebAssembly by default, fetched on first use and cached, so a fresh install renders with nothing else installed. You need a network connection for that first render, and about 6 MB of it. If you’d rather use a system
ffmpegyou already have, the last section shows how to switch. - A directory to work in. keryx keeps no state of its own; everything lives in the project directory you run it from, so a scratch folder is fine.
Install it
go install gitlab.com/phpboyscout/keryx/cmd/keryx@latest
That puts a keryx binary in $(go env GOPATH)/bin:
$ keryx version
Version: v0.11.0
Pre-built archives are attached to each release on GitLab if you’d rather not build from source.
Write the configuration file
keryx refuses to run without a configuration file, and every command fails the same way until one exists:
ERRO failed to load configuration: no config file found hints="Run 'keryx init' to create a configuration."
So do that:
$ keryx init --skip-ai --skip-login --skip-key
INFO Initialising configuration
INFO configuration initialised path=/home/you/.keryx/config.yaml
That writes ~/.keryx/config.yaml with every posting platform disabled, alerting off and no themes. Without those three flags it offers to walk you through credential set-up on an interactive terminal, and you need none of that today.
Have a look at what it wrote. It ships with an example avatar and voice registered under the name matt, which are mine. Harmless, but not yours: delete the avatars: and voices: blocks if you find them confusing.
Add a theme, because there isn’t one
keryx has no built-in themes at all. Palette, fonts, card treatment, music tone and voice settings are configuration, never code, so nothing is seeded and the first render you attempt says so:
no theme configured — add one with `keryx theme add <keyword>` (the first theme added becomes the default)
That’s a deliberate design decision rather than an omission, and one I’ve written about at some length in a different context: shipping a style library inside the binary would mean cutting a release to distribute a change of taste.
Add a minimal one with two palette roles and a card treatment:
keryx theme add starter \
--set 'palette.charcoal=#1A1D21' \
--set 'palette.cream=#F4F1EA' \
--set 'card.mode=block'
added theme "starter"
set as the default theme (it is the only one)
card.mode=block means each card is one line of text over a solid palette colour, which is the treatment that needs no illustration. Exactly what you want with no image provider configured. Check it landed:
$ keryx theme list
themes (default: starter)
* starter user
It went into ~/.keryx/themes.yaml, so it’s available to every project on this machine. A project can carry its own themes in a .keryx.yaml at its root instead.
Create a reel workspace
$ keryx reel new hello-keryx
created reels/hello-keryx
That’s a directory holding everything for one reel: an empty storyboard, the vo/, cards/ and music/ directories, a disposable .cache/, a workspace.yaml that makes it a workspace, and a .gitignore so the throwaway parts stay out of git.
The slug has to be lowercase kebab-case. hello-keryx is fine, Hello Keryx is not.
Write a three-card storyboard
The storyboard is the creative seed: an ordered JSON array, one object per card. Open reels/hello-keryx/storyboard.json, which reel new left as an empty array, and replace it with this:
[
{ "text": "Keryx turns a post", "bg": "charcoal", "fg": "cream", "dur": 2.0 },
{ "text": "into a short reel.", "bg": "charcoal", "fg": "cream", "dur": 2.0 },
{ "text": "keryx.phpboyscout.uk", "bg": "cream", "fg": "charcoal", "mono": true, "dur": 2.5 }
]
Four things worth knowing about those fields. bg and fg are palette role names, not colours, and they have to match roles you defined on the theme, so a role the theme doesn’t define is a validation error. That’s the point of it: change the theme and every card follows. dur is seconds on screen, and it’s only used when there’s no narration, because once a card has a voice-over clip the clip’s length drives the timing instead. mono switches that card to the theme’s monospace font, which is the convention for a closing URL card. And there’s no title or duration field, whatever your fingers think.
Render it
$ keryx reel build --workspace hello-keryx --silent
rendered 3 cards (5.7s) -> reels/hello-keryx/reel-hello-keryx.mp4
assets: 0 selected, 0 locked
--silent says “video only, no narration and no music”, and it’s what makes each card fall back to its dur. On an interactive terminal you’ll see a rendering… NN% line while it works, written to stderr, so piping the output somewhere leaves you with just the result.
The reel is 5.7 seconds rather than the 6.5 you get from adding up the dur values, because cards crossfade into each other by 0.4s and two transitions cost 0.8s.
Check the file is what it claims to be:
$ ffprobe -v error -show_entries stream=width,height,codec_name \
-of default=nw=1 reels/hello-keryx/reel-hello-keryx.mp4
codec_name=h264
width=1080
height=1920
Play it. Three cards, a crossfade between each, no sound.
Change something and watch it change
The build is cheap and repeatable, so this is the loop you’ll spend most of your time in. Swap the palette roles on the last card, run the same build again, and it overwrites the mp4 in place: nothing accumulates, and nothing else in the workspace is touched.
Get a palette role wrong and it stops before rendering anything:
$ keryx reel build --workspace hello-keryx --silent
ERRO invalid storyboard:
- card 1: bg references undefined palette role "nosuchrole"
You can also see what a build would do without doing it:
$ keryx reel plan --workspace hello-keryx
reel plan: hello-keryx [silent fallback (storyboard dur — no promoted VO)]
card dur start vo_delay source
1 2.000 0.000 0.500 storyboard
2 2.000 1.600 2.100 storyboard
3 2.500 3.200 3.700 storyboard
total: 5.700s music bed: 35.000s (35000ms)
assets: 0 selected, 0 locked
5 pre-build gap(s):
- [missing_vo] card 1: no VO — silent fallback
- [missing_vo] card 2: no VO — silent fallback
- [missing_vo] card 3: no VO — silent fallback
- [missing_cover] no cover.png
- [no_music] no music bed
The start column shows the crossfade at work: card 2 starts at 1.6s, not 2.0s. Every one of those gaps is expected here, because each is something you haven’t generated yet.
Switching to a system ffmpeg
The renderer you’ve been using all the way through is afmpeg, FFmpeg compiled to WebAssembly and run entirely in memory. The first render downloads a pinned ffmpeg-wasi module, verifies its SHA-256 and caches it, so only the first run is slow. If you want to know what it does to convince FFmpeg it has a filesystem, that post is the answer.
If you already have a system ffmpeg and would rather keryx shelled out to it, say so:
# ~/.keryx/config.yaml
providers:
render: ffmpeg
One limit worth knowing before you set it: because providers.render is a plain value and providers.render.module is a nested one, you cannot select afmpeg in config and also override its module there. Point at a locally-built or mirrored module with the KERYX_FFMPEG_WASI environment variable instead.
The cheapest possible answer to “does this work”
You now have a real mp4, built from a file you can diff, and you’ve spent nothing at all.
If that suits you, the how-to guides take it stage by stage: narration, a music bed, generated card art, each one gated on your taste rather than switched on wholesale. If it doesn’t, you’ve lost a tea break and learned something, which is a fair trade for fifteen minutes.
I’m still not making one of these for every post, mind, and I’m not certain I ever will. Some of them really are too boringly technical to want a soundtrack. But the ones that deserve a reel now get one inside an evening I actually have, which is a very different position from where this started.
And once you’ve got a reel, posting it to four platforms turns out to be its own problem … which is tomorrow’s.





