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.
Model¶
A theme is a self-contained aesthetic profile identified by a keyword. It has no type — a keyword identifies exactly one theme, anywhere in the catalog (spec 0047 D4).
Every field is optional, and a generation uses whichever ones its target needs. A
cover run ignores music and voice; a reel run does not. A theme without them
is still perfectly usable for a reel — it simply has no music bed, which is a
valid choice.
| Field | Drives |
|---|---|
palette |
role name → hex, referenced by storyboards and card treatments |
style |
the aesthetic: medium, treatment, mood. Not framing or aspect |
card |
the reel card treatment: mode, scrim, fonts, illustration style, sequence, shots |
music |
the bed: prompt, gain |
voice |
the clone: id, stability, similarity, model, pronunciation |
formats |
per-target overrides of the built-in frame — the escape hatch |
Typography travels with the theme¶
card.font_bold and card.font_mono name the faces a card draws with — the body
lines and the closing URL card respectively. They take a font key, never a
path:
A path is a property of one machine; a key is a property of the project. A theme
naming /Users/you/Library/Fonts/Brand.ttf renders correctly for exactly one
person.
Keys resolve through four scopes — project, user, system, and the faces embedded
in the keryx binary — so a theme that names one can say whether the machine
actually has it. Set them with keryx font use,
which resolves the key before writing it; theme edit --set card.font_bold=…
will happily record a font that does not exist, and the mistake surfaces later as
a reel drawn in the fallback face.
Omitting them is fine. Unset, a card draws in inter and jetbrains-mono,
which ship inside the binary — so a theme with no typography at all still renders
anywhere. See keryx font for the scopes and
their trade-offs, and for why the embedded keys cannot be shadowed by the host.
A theme intended for reuse should name a font that is available wherever the theme is: an embedded key always is, a user-library font is available to you everywhere, and a project font is not available outside its project.
Config shape (one flat list):
themes:
default: editorial # the keyword used when --theme is omitted
editorial:
palette: {teal: "#14534F", amber: "#E8923B", cream: "#F2EAD8", charcoal: "#282A2C"}
style: "Editorial conceptual illustration, flat screen-print / risograph…"
card: {mode: overlay, scrim: {from: 0.52, color: charcoal}}
music: {prompt: "restrained editorial bed", gain: 0.16}
voice: {id: MhaH9hcD2Ulcr80j28Z1, stability: 0.6, similarity: 0.92}
Resolution¶
Every generator takes --theme <keyword>. When omitted it falls back to
themes.default. One default, not one per kind of output — a per-target default
would have relocated the type dimension rather than removed it.
An unknown keyword is a hard error — 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.default, 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"}
Upgrading from a typed catalog¶
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.
That layout is no longer read. A catalog still nested as
themes.<type>.<keyword> is refused, by name, with the command to run:
$ keryx theme list
`themes.article` holds a block of themes, which is the pre-0047 layout — run `keryx theme migrate` to flatten it
Refusing is deliberate. Reading both shapes is what let the typed and flat models run side by side for a release, which is the single cause behind four separate bug reports. Ignoring the old block instead would silently drop themes that exist in no other file. So keryx stops, and says what to do.
keryx theme migrate flattens it,
merges any keyword filed under two types, and reports which styles still describe
framing you can now delete. It does not rewrite your prose — a tool that edits
what you wrote is a tool you stop trusting with words.
A flat catalog is not necessarily a finished one. The first pass at this shipped
the flat shape while leaving the pre-0047 fields in place — prompt rather than
style, a redundant aspect: "16:9", and on three themes the aesthetic written out
three times over as prompt + style + card.style. Migration now normalises those
too, and only stops for the one case it cannot decide: a prompt and a style
holding genuinely different text.
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
promoted 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
pinned 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:
The first theme added becomes the default, so one command is enough to start generating:
$ keryx theme add housestyle --set card.style=risograph
added theme "housestyle"
set as the default theme (it is the only one)
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
default 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).