Skip to content

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:

card:
  font_bold: brand
  font_mono: jetbrains-mono

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:

  1. the avatar reference image strongly anchors appearance at the model level (that's what preserves the likeness), and
  2. 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 push already 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 pin copies 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:

$ keryx theme list
themes (default: house)
  emberlight               project
* house                    user

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:

$ keryx theme list
no themes yet — add one with `keryx theme add <keyword>`

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. --set keys are nested paths (card.style, palette.teal, voice.stability), and an unrecognised key is ignored, so check keryx 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).