Themes¶
The thematic component of every artefact keryx generates — image-prompt styles,
palette, music tone, voice — is config, not code. The Python reference
scripts held these as constants (STYLES, PALETTE, the portrait prompt, the
voice settings); keryx lifts them into a theme catalog in config so they can
be added and edited without a rebuild, and so a second brand is just a new set of
themes. Generators resolve a theme by keyword and never carry hardcoded thematic
values (design spec §6).
Model¶
A theme is a self-contained aesthetic profile identified by a keyword and
tagged with a type declaring the artefact it themes. Keywords are unique
within a type, so editorial can name both an article theme and a reel
theme; the command's type disambiguates.
| Type | Drives | Fields |
|---|---|---|
article |
cover art (keryx cover) |
palette, prompt (style prefix), aspect |
reel |
the 9:16 reel (keryx reel build + voice + music) |
palette, card (mode, scrim, fonts, illustration style, sequence, shots), music (prompt, gain), voice (id, stability, similarity, model, pronunciation) |
portrait |
the avatar (keryx portrait) |
palette, prompt |
Types are open-ended — a new generator adds a new type.
Config shape (nested by type → keyword):
themes:
defaults: # the keyword used when --theme is omitted, per type
article: editorial
reel: editorial
portrait: default
article:
editorial: { palette: {...}, prompt: "Editorial conceptual illustration…", aspect: "16:9" }
reel:
editorial:
palette: {...}
card: { mode: overlay, scrim: {from: 0.52, color: charcoal}, style: "…wordless…" }
music: { prompt: "restrained editorial bed", gain: 0.16 }
voice: { id: MhaH9hcD2Ulcr80j28Z1, stability: 0.6, similarity: 0.92 }
portrait:
default: { palette: {...}, prompt: "Stylised editorial avatar…" }
Resolution¶
Every generator takes --theme <keyword>. When omitted it falls back to
themes.defaults.<type> for the command's type. An unknown keyword is a hard
error (it lists what's available) — a typo'd or unregistered --theme never silently
falls back to an unthemed render. Resolution is pure and unit-tested (no I/O), so
timing/wrapping/theme logic stays deterministic (R-GLOBAL-10).
Themes and avatars (cards gen --avatar)¶
When a card is generated with an avatar (cards gen --avatar <name> --theme <kw>),
it is an image-to-image render anchored by the avatar's reference photo. The
theme's card style is woven into the prompt and takes precedence over the
avatar's own configured style — but two things temper how much an off-brand theme
Sequence themes (one evolving scene)¶
By default every card is generated independently, which is right for a kinetic-typography reel but reads as a jumble for a narrative one — style, palette darkness and framing jump card to card. A reel theme can instead declare its cards a sequence:
card:
mode: overlay
sequence: true
shots: # optional; a built-in rotation is used if omitted
- "a wide shot with generous negative space; the subject small in the frame"
- "an intimate close shot; fills the frame, little background"
- "an extreme close-up on a detail — hands, an object, a fragment"
How it works. Card 1 is the style anchor: generate it, pick a take, and
every later card is generated with that promoted image attached as a reference,
so the look holds across the board. Running cards gen for the whole board
before card 1 is picked generates only card 1 and says so — committing a dozen
generations against an unproven anchor is the waste this avoids.
Re-rolling a single card (cards gen --card 5) regenerates just that card,
against the same anchor. Re-rolling card 1 regenerates the anchor itself,
which changes the reference the rest of the board was built against.
Why shots matters. Anchoring alone holds the look but clones the
compositions — that was the observed failure: consecutive panels came out
near-identical and the later beats had no room. So each card also carries a
framing directive, cycled from shots, giving the sequence rhythm: wides that
breathe, an intimate close, an extreme close-up. The directive is appended to the
card's own scene, never replacing it.
The anchor is deliberately card 1's promoted image, not a separate asset — it already lives in the workspace and
media pushalready carries it.
restyles an avatar card:
- the avatar reference image strongly anchors appearance at the model level (that's what preserves the likeness), and
- avatar prompts carry a fixed flat-illustration directive to keep the house look.
So a far-off-brand theme (e.g. a neon palette over the editorial house style) will
restyle an avatar card only weakly. For a strong off-brand look, use scene-only
cards (cards gen without --avatar), or tune the avatar's own style/palette.
Scene-only cards apply the theme's style prefix directly with no reference anchor.
Where themes live¶
keryx init seeds no themes. keryx has no programmatic knowledge of any
theme — resolution is --theme <keyword>, then themes.defaults.<type>, then a
clear error — so shipping a style library inside the binary would mean cutting a
release to distribute a change of taste, and every install of a reusable tool
inheriting one project's aesthetic (spec 0046).
One list, whatever you are making¶
A theme is an aesthetic — palette, medium, treatment. It is not tied to a
kind of output: the same linocut styles a blog cover, a reel card and your
avatar, because what differs between those is format, not taste.
keryx supplies the format from what you are generating (spec 0047):
| Target | Aspect | Composition |
|---|---|---|
| cover | 16:9 | subject centred, generous negative space |
| card | 9:16 | negative space in the lower half for an overlaid caption |
| portrait | 1:1 | head and shoulders, framed to crop to a circular avatar |
So your style text should describe how it looks, not how it is cropped. If a theme genuinely needs an unusual frame, override it:
themes:
panorama:
style: Sweeping painted vista, muted dusk palette.
formats:
cover: {aspect: "21:9"}
Themes used to be typed —
article,reel,portrait— and the same aesthetic had to be written out once per type, differing only in its framing clause. If your catalog is still that shape it keeps working;keryx theme migrateflattens it and tells you which styles still describe framing you can now delete.
Scopes¶
Themes live in two scopes:
| Scope | File | Purpose |
|---|---|---|
| User | ~/.keryx/themes.yaml |
your style library, shared across your projects |
| Project | <project>/.keryx.yaml |
themes authored here, before they graduate |
A project inherits the user's themes, and its own definitions win on a clash.
New themes are created in the project — that is where the work is, against a real reel. Good ones then move up to your library.
CI does not need themes. Generation is local; CI publishes an already-generated reel, and nothing on the publish path resolves a theme. An earlier version of this page said a project should carry the themes it uses "because CI has no user config" — true but irrelevant, unless you actually generate on a runner. If you do,
keryx theme pincopies one back down.
keryx theme list shows which scope each theme resolves from, so "where does
this come from?" is answerable before you edit or promote one:
Moving a theme between scopes¶
Themes are usually authored in a project, against a real reel.
keryx theme promote lifts one into your
user library so every project can use it. It moves the entry — the project
stops carrying its own copy:
$ keryx theme promote emberlight --type reel
promoted reel theme "emberlight" to your user library
written to /home/you/.keryx/themes.yaml
moved out of /home/you/blog/.keryx.yaml — this project no longer carries its own copy
The studio does the same thing: Project settings → Themes shows each theme's scope, with a Promote button on project-scoped ones.
Where the files are¶
If your themes still live in ~/.keryx/config.yaml, they resolve fine — keryx
reads that as a lower-precedence layer — but the split is not finished.
keryx theme migrate moves them into
themes.yaml, and keryx doctor will remind you until it is done.
The other direction is keryx theme pin:
$ keryx theme pin neon --type article
pinned article theme "neon" to this project
commit it so CI resolves the same theme you do
CI sees only the project's config, so a reel whose theme lives in your user library builds on your machine and fails on the runner. Pinning makes the project's dependency on a style explicit and committable. Your library keeps its copy, and inside the project the pinned one wins — so what you preview locally is what CI builds.
Starting from nothing¶
A fresh install has an empty catalog, and says so:
$ keryx theme list
no themes yet — add one with `keryx theme add <keyword> --type reel|article|portrait`
The first theme added for a type becomes that type's default, so one command is enough to start generating:
$ keryx theme add housestyle --type reel --set card.style=risograph
added reel theme "housestyle"
set as the default reel theme (it was the first)
An existing default is never taken over by a later theme add — that would
silently change what every generator resolves to. To change it, use
keryx theme use:
$ keryx theme use emberlight --type reel
default reel theme is now "emberlight"
written to /home/you/.keryx/themes.yaml
It writes to whichever scope the default already comes from, and says which file.
--project / --user pin it explicitly — --project is how a repo carries its
own default, so anyone working in it resolves the same theme you do.
A theme needs at least one setting.
--setkeys are nested paths (card.style,palette.teal,voice.stability), and an unrecognised key is ignored, so checkkeryx theme show <keyword>if a setting seems not to apply.
Managing themes¶
Use keryx theme to list, show, add (or clone with
--from), edit (--set key=value), and remove themes. Edits persist through the
GTB config layer and are picked up live via config hot-reload; secrets are never
written to the theme config.
Parity note¶
The seeded values are intended to be identical to the Python scripts. The
per-card reel illustration style strings and the clay/blueprint music
prompts are derived from the cover styles / spec examples (the Python scripts did
not yet generate per-card imagery); they are seeds to tune via keryx theme edit,
not fixed constants. See PHASE-DECISIONS-LOG.md (Phase 1a).