Themes¶
The thematic part 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 moves them into a theme catalog in config, so they can
be added and edited without a rebuild, and a second brand is 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
still works for a reel. It 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, for a theme that needs an unusual one |
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 keryx can tell whether the machine actually has the face a
theme names. 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 at the
next reel build, which refuses before generating anything.
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. There is one default, not one per kind of output, because a
per-target default would have moved the type dimension rather than removed it.
An unknown keyword is a hard error. A mistyped or unregistered --theme never
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. Two things limit how far an off-brand theme
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.
Sequence themes (one evolving scene)¶
By default every card is generated independently. That is right for a kinetic-typography reel, but a narrative one reads as a jumble: style, palette darkness and framing jump from 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. Paying for 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 failure seen in practice: consecutive panels came out
near-identical and the later beats had no room. Each card therefore also carries a
framing directive, cycled from shots, which varies the framing along the sequence:
a wide shot with space around the subject, an intimate close, an extreme close-up.
The directive is appended to the card's own scene and never replaces 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.
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. 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, because that is where the work is, against a real reel. Good ones then move up to your library.
Publishing from 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". That is true, but it only matters if you generate or build 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, so 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, because keryx
reads that file as a lower-precedence layer. The split is not finished, though.
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 if you build on a runner, a reel whose theme lives in your user library builds on your machine and fails there. 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)
A later theme add never replaces an existing default, because that would
change what every generator resolves to without telling you. 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 and --user choose the scope 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. Each command writes to the
file of the scope it changes, and a CLI run reads the files fresh. The studio picks
up an edit to a project's .keryx.yaml without a restart. Secrets are never written
to the theme config.
Parity note¶
keryx seeds no themes, so parity lives in the blog's own theme library. Its values
were pinned to be identical to the Python scripts. The per-card reel
illustration style strings and the clay and blueprint music prompts were
derived from the cover styles and spec examples, because the Python scripts did not
yet generate per-card imagery. They are starting points to tune with
keryx theme edit, not fixed constants. See PHASE-DECISIONS-LOG.md (Phase 1a).