Documentation
¶
Overview ¶
Package theme models keryx's config-driven aesthetic profiles.
A theme is a self-contained, keyword-identified aesthetic profile. The thematic component of every generated artefact — image-prompt styles, palette, music tone, voice — lives in the theme catalog in config, never as Go constants (spec 0001 §6). Generators resolve a theme by keyword, falling back to the single configured default, and never carry hardcoded thematic values.
A theme has NO TYPE (spec 0047 D4). An aesthetic is not a kind of artefact: what makes a cover 16:9 and a card 9:16 is the TARGET being generated, and that lives in code (see format.go). One list, any theme usable anywhere.
The pre-0047 `themes.<type>.<keyword>` shape is refused at load and read only by `theme migrate` — see legacy.go.
Index ¶
- Constants
- Variables
- func IsEmpty(th Theme) (bool, error)
- func PaletteKeys(th Theme) map[string]bool
- func ToMap(th Theme) (map[string]any, error)
- type Card
- type Catalog
- func (c *Catalog) Add(th Theme) error
- func (c *Catalog) Changes() ([]config.Change, error)
- func (c *Catalog) Get(keyword string) (Theme, error)
- func (c *Catalog) Keywords() []string
- func (c *Catalog) List() []Theme
- func (c *Catalog) Remove(keyword string) error
- func (c *Catalog) Resolve(keyword string) (Theme, error)
- func (c *Catalog) Set(th Theme)
- func (c *Catalog) SetDefault(keyword string) error
- type FlatSummary
- type Format
- type LegacyCatalog
- func (l *LegacyCatalog) Default() string
- func (l *LegacyCatalog) DefaultConflict() []string
- func (l *LegacyCatalog) Divergent() []string
- func (l *LegacyCatalog) Flat() []Theme
- func (l *LegacyCatalog) FlatChanges(chosenDefault string) ([]config.Change, FlatSummary, error)
- func (l *LegacyCatalog) Has(keyword string) bool
- func (l *LegacyCatalog) IsFlat() bool
- func (l *LegacyCatalog) Keywords() []string
- type Music
- type Palette
- type Scrim
- type Target
- type Theme
- type Voice
Constants ¶
const Hardening = "Absolutely no text, letters, numbers, words, captions, " +
"labels, logos, watermarks or signatures anywhere in the image — purely visual."
Hardening is appended to every generated image prompt. It lives here rather than in a theme because "no words in the picture" is a property of what keryx makes, not of anyone's taste — and a theme that had to restate it would be a theme that could forget to.
Variables ¶
var ( // ErrNotFound is returned when a theme keyword is absent. ErrNotFound = errors.NewSentinel("keyrx.not_found", "theme not found") // ErrExists is returned by Add when the keyword already exists. // // Under 0047 this is the ONLY answer to a duplicate keyword. Before the // flattening, `theme add --type article editorial` was accepted alongside an // untyped `editorial` and then lost the resolution race, so the theme you // added was unreachable and `theme show` could not display it either // (keryx #12). One list means one holder of a name. ErrExists = errors.NewSentinel("keyrx.exists", "theme already exists") // ErrIsDefault is returned by Remove when the theme is the catalog default. ErrIsDefault = errors.NewSentinel("keyrx.is_default", "theme is the default") // ErrNoDefault is returned by Resolve when no keyword is given and no default // is configured. Themes are not seeded (spec 0046), so on a fresh install this // is the FIRST thing a user meets — it has to say what to do. ErrNoDefault = errors.New( "no theme configured — add one with `keryx theme add <keyword>` " + "(the first theme added becomes the default)") // ErrReserved is returned when a theme's keyword collides with a config key // that means something else in the flat shape. ErrReserved = errors.NewSentinel("keyrx.reserved_keyword", "theme keyword is reserved") // ErrLegacyShape is returned by Load for a pre-0047 `themes.<type>.<keyword>` // catalog. // // Refusing is deliberate, and it is the only safe option of three. Reading it // is what allowed the typed and flat models to run side by side, which is the // root cause behind keryx #10, #12, #13 and #14. Ignoring it would silently // drop themes that exist in no other layer. So: stop, and say the command. ErrLegacyShape = errors.NewSentinel("keyrx.legacy_themes", "pre-0047 theme layout") )
Sentinel errors for catalog operations (callers map these to exit codes / hints; see the CLI contracts in 0055).
var ErrBadSet = errors.NewSentinel("keyrx.bad_set", "invalid set (want key=value)")
ErrBadSet is returned when a --set argument is not key=value.
var ErrReservedKeyword = errors.NewSentinel("keyrx.reserved_keyword", "theme keyword collides with a reserved key")
ErrReservedKeyword is returned when a theme's keyword collides with a reserved config key in the flat shape.
var ErrUnknownTarget = fmt.Errorf("unknown generation target")
ErrUnknownTarget is returned for a target with no format profile.
Functions ¶
func IsEmpty ¶ added in v0.9.0
IsEmpty reports whether a theme carries no settings at all. Every field is omitempty, so such a theme serialises to an empty map — which the YAML emitter refuses, with an error about braces that tells the user nothing about themes. Callers check this first so they can say something useful instead.
func PaletteKeys ¶
PaletteKeys returns the theme's defined palette-role names as a set — the input to storyboard palette-role validation (R-WS-12). Used by both the renderer and the studio so the check stays identical across surfaces.
Types ¶
type Card ¶
type Card struct {
// Mode is "overlay" (full-bleed illustration + scrim + text) or "block"
// (one line over a solid palette background).
Mode string `json:"mode" mapstructure:"mode" yaml:"mode"`
// Scrim is the overlay gradient (overlay mode only).
Scrim *Scrim `json:"scrim,omitempty" mapstructure:"scrim" yaml:"scrim,omitempty"`
// Lead / Tail are the silence a card holds before and after its narration,
// in seconds (spec 0050 D4). Zero means the parity defaults, 0.5 and 0.7.
// Pacing is part of a look, which is why it lives with one.
Lead float64 `json:"lead,omitempty" mapstructure:"lead" yaml:"lead,omitempty"`
Tail float64 `json:"tail,omitempty" mapstructure:"tail" yaml:"tail,omitempty"`
// FontBold / FontMono are the card fonts (bold for body lines, mono for the
// URL closer).
FontBold string `json:"font_bold,omitempty" mapstructure:"font_bold" yaml:"font_bold,omitempty"`
FontMono string `json:"font_mono,omitempty" mapstructure:"font_mono" yaml:"font_mono,omitempty"`
// Style is the illustration-generation prompt prefix for per-card scenes,
// rendered in this theme's visual style at 9:16 (spec 0001 §3.1).
Style string `json:"style,omitempty" mapstructure:"style" yaml:"style,omitempty"`
// Sequence marks this theme's cards as ONE evolving scene rather than
// independent illustrations (spec 0045 D2). Card 1 becomes the style anchor
// and every later card is generated against it, so the look holds across the
// board. Off by default: a kinetic-typography theme wants independent cards.
Sequence bool `json:"sequence,omitempty" mapstructure:"sequence" yaml:"sequence,omitempty"`
// Shots are per-beat framing directives cycled across a sequence — wides that
// breathe, an intimate close, an extreme close-up. Anchoring alone holds the
// look but CLONES the compositions (0045 §2 stage 2); varying the shot scale
// and negative space per beat is the other half of the fix (D6). Ignored
// unless Sequence is set; empty falls back to a built-in rotation.
Shots []string `json:"shots,omitempty" mapstructure:"shots" yaml:"shots,omitempty"`
}
Card is the reel card visual treatment.
type Catalog ¶
type Catalog struct {
// Default is `themes.default` — the keyword used when a generation names
// none. One default, not one per target: a per-target default would have
// relocated the type dimension rather than removed it (0047 D1).
Default string
// Themes maps keyword → theme (`themes.<keyword>`).
Themes map[string]Theme
}
Catalog is the in-memory theme catalog: one flat keyword→theme map and a single default keyword (spec 0047 §3.1, D1). It is loaded from config, mutated purely in memory, and serialised back. All read methods are pure and side-effect free (spec 0001 §8 / R-GLOBAL-10).
func Load ¶
Load parses the theme catalog out of a config reader (spec 0035: go/config's read surface — a Store's View, or any Reader). A missing themes block yields an empty catalog, not an error (a fresh project before init).
A pre-0047 nested catalog is refused with ErrLegacyShape. `theme migrate` reads that shape through LoadLegacy instead — it is the one caller that should still understand it.
func (*Catalog) Add ¶
Add inserts a new theme, erroring if the keyword is taken or reserved.
The first theme in an empty catalog also becomes the default. Themes are not seeded (0046), so without that a fresh install would need an explicit --theme on every generation. An EXISTING default is never touched: adding a theme is an additive act and must not change what an unrelated workflow resolves to (keryx #14).
func (*Catalog) Changes ¶ added in v0.9.0
Changes returns the config write operations that persist this catalog under "themes" (spec 0035): a single whole-subtree Set applied via config.Store.Apply, which routes it to the writable layer and preserves the rest of the file. Replacing the whole subtree is what makes removals take effect.
NOTE: a whole-subtree Set does not preserve comments/anchors *within* themes (go/config write-fidelity). Targeted per-theme Set/Remove is the 0035 refinement.
func (*Catalog) Remove ¶
Remove deletes a theme. It refuses to remove the catalog default unless the default is reassigned first (R-THEME-5).
func (*Catalog) Resolve ¶
Resolve returns the theme to generate with: the named keyword, or — when none is named — the configured default. This is the resolution every generator uses.
It takes no target. A target selects a FORMAT (see Theme.FormatFor); it does not select an aesthetic, which is the whole of 0047.
func (*Catalog) SetDefault ¶
SetDefault makes an existing theme the catalog default.
type FlatSummary ¶ added in v0.9.0
type FlatSummary struct {
// Keywords is every distinct theme, sorted.
Keywords []string `json:"keywords"`
// Merged maps a keyword to the types it was flattened from, for the
// collisions. Reported so a merge is visible rather than silent.
Merged map[string][]string `json:"merged,omitempty"`
// Default is the single default keyword the flat catalog will carry.
Default string `json:"default"`
// Framing maps a keyword to the format-ish phrases still in its style — the
// instructions the target now supplies. Reported for MANUAL editing: the tool
// does not rewrite prose a human wrote (D2).
//
// The PHRASES matter, not just the keyword. Nearly every style mentions
// something, so a bare list of names flags everything and tells you nothing;
// "editorial: 16:9, banner" tells you what to delete.
Framing map[string][]string `json:"framing,omitempty"`
// Renames lists keywords that need a human to choose a new name — see
// [needsRename].
Renames []string `json:"renames,omitempty"`
// Divergent lists keywords whose `prompt` and `style` hold DIFFERENT prose.
// Both are left in place: only one can survive and the choice is the author's,
// so this reports rather than resolves (the same line D2 draws around framing).
Divergent []string `json:"divergent,omitempty"`
}
FlatSummary describes what flattening a catalog did, for a migration to report.
type Format ¶ added in v0.9.0
type Format struct {
// Aspect is the frame shape, e.g. "16:9".
Aspect string `json:"aspect,omitempty" mapstructure:"aspect" yaml:"aspect,omitempty"`
// Composition directs framing and negative space. Appended to the theme's
// style, never replacing it.
Composition string `json:"composition,omitempty" mapstructure:"composition" yaml:"composition,omitempty"`
}
Format is the composition a target needs.
type LegacyCatalog ¶ added in v0.11.0
type LegacyCatalog struct {
// contains filtered or unexported fields
}
LegacyCatalog is an opaque handle on a pre-0047 catalog, produced by LoadLegacy and consumed by LegacyCatalog.FlatChanges. Nothing else can see inside it, which is what keeps the type dimension out of the rest of keryx.
func LoadLegacy ¶ added in v0.11.0
func LoadLegacy(r config.Reader) (*LegacyCatalog, error)
LoadLegacy reads a catalog in either shape — nested, flat, or a mix — for migration. Unlike Load it never refuses: reading the old layout is precisely what it is for.
func (*LegacyCatalog) Default ¶ added in v0.11.0
func (l *LegacyCatalog) Default() string
Default returns the single default this catalog implies: an already-migrated `themes.default`, else the pre-0047 per-type default. Empty when it has none.
func (*LegacyCatalog) DefaultConflict ¶ added in v0.11.0
func (l *LegacyCatalog) DefaultConflict() []string
DefaultConflict reports the distinct per-type defaults when they disagree.
Migration must NOT silently pick one: the default decides what every un-themed generation produces, so choosing on the user's behalf changes output they never asked to change. Returns nil when there is nothing to ask about.
func (*LegacyCatalog) Divergent ¶ added in v0.11.0
func (l *LegacyCatalog) Divergent() []string
Divergent returns the keywords whose `prompt` and `style` hold DIFFERENT prose, sorted. Each needs a human to decide which wording survives.
func (*LegacyCatalog) Flat ¶ added in v0.11.0
func (l *LegacyCatalog) Flat() []Theme
Flat returns the catalog as one list in the 0047 shape — merging a keyword that appears under more than one type, and normalising each theme's fields. This is what a migration writes.
func (*LegacyCatalog) FlatChanges ¶ added in v0.11.0
func (l *LegacyCatalog) FlatChanges(chosenDefault string) ([]config.Change, FlatSummary, error)
FlatChanges renders the catalog in the 0047 shape: `themes.<keyword>` plus a single `themes.default`, with the typed blocks removed.
chosenDefault resolves a conflict between the pre-0047 per-type defaults. Empty means "no conflict, or already decided" — LegacyCatalog.DefaultConflict is how a caller finds out whether it must ask.
func (*LegacyCatalog) Has ¶ added in v0.11.0
func (l *LegacyCatalog) Has(keyword string) bool
Has reports whether a keyword is present under any type.
func (*LegacyCatalog) IsFlat ¶ added in v0.11.0
func (l *LegacyCatalog) IsFlat() bool
IsFlat reports a catalog with nothing left to migrate. A migration that says "nothing to do" is more useful than a no-op that looks like work — but it has to be TRUE, and "flat" is not the same as "done".
Flattening the SHAPE was only half of 0047. The reference library came out of P1–P4 structurally flat while every theme still carried the pre-0047 FIELDS: a redundant `aspect`, `prompt` instead of `style`, and in three cases the aesthetic written out three times over. Answering "flat?" with "yes" there is what left it in that state, so this asks the fuller question.
func (*LegacyCatalog) Keywords ¶ added in v0.11.0
func (l *LegacyCatalog) Keywords() []string
Keywords returns every distinct keyword across all types, sorted.
type Music ¶
type Music struct {
// Prompt is the tone-matched generation prompt for the bed (bespoke per
// reel in practice; this is the seeded default tone).
Prompt string `json:"prompt,omitempty" mapstructure:"prompt" yaml:"prompt,omitempty"`
// Gain is the bed's mix level under the VO (≈0.16).
Gain float64 `json:"gain" mapstructure:"gain" yaml:"gain"`
}
Music is the reel's music-bed settings.
type Palette ¶
Palette maps a role name (e.g. "teal", "amber", "cream", "charcoal") to a hex colour. Roles are referenced by name from storyboards and card treatments, so the names are part of the contract, not the hex values.
type Scrim ¶
type Scrim struct {
// From is where the gradient STARTS — fully transparent — as a fraction of
// card height measured from the top (0=top, 1=bottom). It ramps to full
// opacity at the frame edge Direction points to; 0.52 ≈ the lower half.
From float64 `json:"from" mapstructure:"from" yaml:"from"`
// Color is the palette role the scrim fades to (usually "charcoal").
Color string `json:"color" mapstructure:"color" yaml:"color"`
// Opacity is the gradient's maximum alpha, 0–1. Zero means the default.
Opacity float64 `json:"opacity,omitempty" mapstructure:"opacity" yaml:"opacity,omitempty"`
// Direction is which edge the gradient ramps to full opacity at: "up"
// (the default — transparent at From, solid at the bottom) or "down"
// (solid at the top). Empty means "up".
Direction string `json:"direction,omitempty" mapstructure:"direction" yaml:"direction,omitempty"`
}
Scrim is the gradient overlay applied over an overlay card's illustration so the text stays legible: a transparent→color gradient across part of the frame.
A theme's scrim is the default for every overlay card it styles; a card may override any field (spec 0050 D6).
type Target ¶ added in v0.9.0
type Target string
Target is what is being generated. It selects a Format.
const ( // TargetCover is a blog post's cover image — wide, standalone. TargetCover Target = "cover" // TargetCard is a reel card illustration — vertical, with a caption over it. TargetCard Target = "card" // TargetPortrait is an avatar, generated image-to-image from reference // photos. The person comes from the avatar registry's Likeness, not from // here — a portrait is a format, not a description of anybody (0047 D3). TargetPortrait Target = "portrait" )
type Theme ¶
type Theme struct {
Keyword string `json:"keyword" mapstructure:"-" yaml:"-"`
Palette Palette `json:"palette,omitempty" mapstructure:"palette" yaml:"palette,omitempty"`
// Style is the aesthetic: medium, treatment, mood. NOT framing or aspect —
// those come from the target's format profile (spec 0047, see format.go), so
// one style serves a cover, a card and an avatar alike.
Style string `json:"style,omitempty" mapstructure:"style" yaml:"style,omitempty"`
// Formats optionally overrides a target's built-in composition profile,
// keyed by target ("cover", "card", "portrait"). The escape hatch for a
// style whose whole point is an unusual crop.
Formats map[string]Format `json:"formats,omitempty" mapstructure:"formats" yaml:"formats,omitempty"`
// Prompt and Aspect are the PRE-0047 FIELD names, still read because
// flattening the catalog's SHAPE (0047 §6) did not rewrite the prose inside
// it — the reference install is flat and still writes `prompt:`. Prompt falls
// back into Style; Aspect is folded into Formats["cover"] at load, so a
// non-default frame is honoured rather than silently lost. `theme migrate`
// writes the modern names; nothing else does.
Prompt string `json:"prompt,omitempty" mapstructure:"prompt" yaml:"prompt,omitempty"`
Aspect string `json:"aspect,omitempty" mapstructure:"aspect" yaml:"aspect,omitempty"`
// MergedFrom records the pre-0047 types this keyword was flattened from, so
// `theme migrate` can report what it merged rather than silently conflating
// two entries. Set only on the legacy read path; never persisted.
MergedFrom []string `json:"merged_from,omitempty" mapstructure:"-" yaml:"-"`
// Card / Music / Voice are consulted when generating a reel. A theme without
// them is still usable for one — it simply has no music bed, which is a valid
// choice (0047 D4). Avatars (the actor capability) are a separate top-level
// registry (internal/avatar), not a field here.
Card *Card `json:"card,omitempty" mapstructure:"card" yaml:"card,omitempty"`
Music *Music `json:"music,omitempty" mapstructure:"music" yaml:"music,omitempty"`
Voice *Voice `json:"voice,omitempty" mapstructure:"voice" yaml:"voice,omitempty"`
}
Theme is a single aesthetic profile. Every field is optional and a generator uses whichever ones its target needs — a cover run ignores Music and Voice, a card run ignores nothing. Keyword is the catalog coordinate and is not serialised into the theme's value (it is its location).
func ApplySet ¶
ApplySet returns a copy of th with the given dotted key=value assignments applied (e.g. "voice.stability=0.74", "palette.amber=#E8923B", "card.mode=block"). Values are coerced to bool / int / float when they parse as such, else kept as strings. Keyword is preserved (it is the catalog coordinate, not a settable field).
func (Theme) FormatFor ¶ added in v0.9.0
FormatFor returns the profile for a target, with any per-theme override applied. An unknown target is an error rather than a silent default: guessing a frame shape produces a plausible image of the wrong dimensions, which is worse than refusing.
func (Theme) PromptFor ¶ added in v0.9.0
PromptFor assembles the image prompt for a target.
subject is what the author wrote — a scene, or a cover brief. It is never replaced or reworded, only surrounded: the author's intent is the one part of this that keryx has no business editing.
func (Theme) StyleFor ¶ added in v0.9.0
StyleFor returns the aesthetic text this theme contributes, tolerating an unmigrated catalog.
Resolution order is deliberate: the 0047 `style`, else the reel `card.style`, else the pre-0047 `prompt`. A half-migrated theme therefore keeps working rather than silently generating with no style at all — which would look like a provider fault, not a config one.
type Voice ¶
type Voice struct {
// ID is the provider voice identifier (e.g. an ElevenLabs voice id).
ID string `json:"id,omitempty" mapstructure:"id" yaml:"id,omitempty"`
// Stability / Similarity are the clone settings (higher similarity favours
// fidelity to the reference voice).
Stability float64 `json:"stability" mapstructure:"stability" yaml:"stability"`
Similarity float64 `json:"similarity" mapstructure:"similarity" yaml:"similarity"`
// Style is expression/exaggeration (0 = most faithful; higher is more
// expressive but can drift the accent). Speed is the speech rate (0.7 slow …
// 1.2 fast; 0 falls back to the provider default 1.0). SpeakerBoost toggles
// speaker enhancement; nil falls back to the provider default (on).
Style float64 `json:"style,omitempty" mapstructure:"style" yaml:"style,omitempty"`
Speed float64 `json:"speed,omitempty" mapstructure:"speed" yaml:"speed,omitempty"`
SpeakerBoost *bool `json:"speaker_boost,omitempty" mapstructure:"speaker_boost" yaml:"speaker_boost,omitempty"`
// Model selects the TTS model for this theme's voice (empty ⇒ provider default,
// eleven_multilingual_v2). Pronunciation references a provider pronunciation
// dictionary id whose rules apply to every line (spec 0022 B, A).
Model string `json:"model,omitempty" mapstructure:"model" yaml:"model,omitempty"`
Pronunciation string `json:"pronunciation,omitempty" mapstructure:"pronunciation" yaml:"pronunciation,omitempty"`
}
Voice is the reel narration (voice-clone) settings.