Skip to content

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 push already carries it.

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.

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 migrate flattens 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 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 --type reel
reel (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 — 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. --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. 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).