Files
Kevin Gilper 070b838385 Add designer artwork and edit-composite recipe packs
Fifteen new bundled recipes, all render-validated under the stack's
default smart per-card timing: a ten-recipe designer artwork pack
(poster style, soft media, pattern energy, era print look, paper and
canvas, metallic accents, ornament borders, stained glass, and the
style-timing pair) and a five-recipe edit-and-composite pack (scene
light, monochrome look, atmosphere, background only, carry the subject
over). Recipe Kit README gains the smart-timing tuning rule (early is
the structure-phase volume; motif-scale borrows need early ~0.85), the
Builder reserves the new labels, and the docs list the full bundled
roster. V9 untouched.
2026-07-06 21:43:00 -04:00

569 lines
31 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Krea Recipe Builder</title>
<style>
:root {
--bg: #101216; --panel: #181b21; --panel-2: #1f232b; --line: #2a2f39;
--text: #e8eaee; --muted: #9aa3b2; --accent: #6fb2ff; --accent-2: #8ee0c2;
--warn: #ffb86b; --err: #ff7d7d; --code-bg: #0b0d10;
--radius: 12px; font-size: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--text);
font: 15px/1.55 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
header { padding: 28px 22px 10px; max-width: 1180px; margin: 0 auto; }
header h1 { margin: 0 0 6px; font-size: 26px; letter-spacing: 0.2px; }
header p { margin: 0; color: var(--muted); max-width: 70ch; }
main {
max-width: 1180px; margin: 18px auto 60px; padding: 0 22px;
display: grid; grid-template-columns: minmax(0, 1fr) 400px; gap: 20px;
align-items: start;
}
@media (max-width: 940px) { main { grid-template-columns: 1fr; } .out { position: static !important; } }
.card {
background: var(--panel); border: 1px solid var(--line);
border-radius: var(--radius); padding: 18px 18px 16px; margin-bottom: 18px;
}
.card h2 { margin: 0 0 4px; font-size: 17px; }
.card h2 .step { color: var(--accent); margin-right: 8px; }
.card > p.hint { margin: 0 0 14px; color: var(--muted); font-size: 13.5px; }
label.field { display: block; font-size: 13px; color: var(--muted); margin: 10px 0 4px; }
input[type="text"] {
width: 100%; padding: 9px 11px; border-radius: 8px; border: 1px solid var(--line);
background: var(--panel-2); color: var(--text); font-size: 14.5px;
}
input[type="text"]:focus { outline: 1px solid var(--accent); }
.filehint { font-size: 12.5px; color: var(--muted); margin-top: 5px; }
.error { color: var(--err); font-size: 13px; margin-top: 6px; display: none; }
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 10px; }
.tile {
border: 1px solid var(--line); background: var(--panel-2); border-radius: 10px;
padding: 11px 12px; cursor: pointer; transition: border-color .12s, background .12s;
}
.tile:hover { border-color: #3b4250; }
.tile.sel { border-color: var(--accent); background: #1c2634; }
.tile b { display: block; font-size: 14px; margin-bottom: 3px; }
.tile span { font-size: 12px; color: var(--muted); line-height: 1.4; display: block; }
.honesty {
margin-top: 14px; border-left: 3px solid var(--accent-2); background: var(--panel-2);
border-radius: 0 8px 8px 0; padding: 10px 12px; font-size: 13.5px; display: none;
}
.honesty b { color: var(--accent-2); }
.seg { display: flex; gap: 8px; flex-wrap: wrap; }
.seg button {
flex: 1 1 120px; padding: 10px 10px 8px; border-radius: 10px; cursor: pointer;
border: 1px solid var(--line); background: var(--panel-2); color: var(--text);
font: inherit; text-align: left;
}
.seg button.sel { border-color: var(--accent); background: #1c2634; }
.seg button b { display: block; font-size: 14px; }
.seg button span { font-size: 12px; color: var(--muted); }
details {
border: 1px solid var(--line); border-radius: 10px; background: var(--panel-2);
padding: 10px 14px; margin-top: 6px;
}
details summary { cursor: pointer; font-size: 14px; color: var(--muted); }
details[open] summary { margin-bottom: 8px; }
.adv-row { display: flex; align-items: center; gap: 10px; margin: 12px 0; flex-wrap: wrap; }
.adv-row label { font-size: 13.5px; min-width: 200px; }
.adv-row input[type="range"] { flex: 1; min-width: 140px; accent-color: var(--accent); }
.adv-row output { font-size: 13px; color: var(--accent); min-width: 40px; text-align: right; }
.adv-row input[type="checkbox"] { accent-color: var(--accent); width: 16px; height: 16px; }
.adv-note { font-size: 12.5px; color: var(--muted); margin: 2px 0 0; }
.out { position: sticky; top: 14px; }
.out .card { margin-bottom: 14px; }
.tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.tabs button {
padding: 5px 14px; border-radius: 999px; border: 1px solid var(--line);
background: var(--panel-2); color: var(--muted); cursor: pointer; font: inherit; font-size: 13px;
}
.tabs button.sel { color: var(--text); border-color: var(--accent); }
pre#code {
background: var(--code-bg); border: 1px solid var(--line); border-radius: 10px;
padding: 13px 14px; margin: 0; font: 12.5px/1.55 ui-monospace, Consolas, monospace;
color: #d7e3f4; overflow-x: auto; max-height: 430px;
}
.btns { display: flex; gap: 10px; margin-top: 12px; }
.btns button {
flex: 1; padding: 10px; border-radius: 9px; font: inherit; font-size: 14px;
cursor: pointer; border: 1px solid var(--line); background: var(--panel-2); color: var(--text);
}
.btns button.primary { background: var(--accent); border-color: var(--accent); color: #0b1523; font-weight: 600; }
.btns button:disabled { opacity: 0.45; cursor: not-allowed; }
ol.install { margin: 8px 0 0; padding-left: 20px; font-size: 13.5px; color: var(--muted); }
ol.install li { margin-bottom: 5px; }
ol.install code, .testbox code, .honesty code { background: var(--code-bg); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
.testbox { font-size: 13.5px; color: var(--muted); }
.testbox b { color: var(--text); }
footer { max-width: 1180px; margin: 0 auto 40px; padding: 0 22px; color: var(--muted); font-size: 12.5px; }
footer a { color: var(--accent); }
.copied { color: var(--accent-2) !important; border-color: var(--accent-2) !important; }
</style>
</head>
<body>
<header>
<h1>Krea Recipe Builder</h1>
<p>
Answer three questions and download a ready-to-use recipe for the
<strong>KG Krea 2 Image Guide Card V10</strong>. Every number is filled in
from the render-validated tuning tables &mdash; no knowledge of treatments,
pulls, or layer arrays required.
</p>
</header>
<main>
<div class="steps">
<div class="card">
<h2><span class="step">1</span>Name it</h2>
<p class="hint">The name becomes the choice in the card's <code>Use image for</code> dropdown.</p>
<label class="field" for="label">Recipe name</label>
<input type="text" id="label" placeholder="e.g. warm autumn palette" maxlength="60" autocomplete="off">
<div class="filehint" id="filehint"></div>
<div class="error" id="labelerr"></div>
<label class="field" for="desc">One-line description <span style="opacity:.6">(optional, for humans reading the file)</span></label>
<input type="text" id="desc" placeholder="e.g. Cozy oranges and teals without copying the photo" maxlength="140" autocomplete="off">
</div>
<div class="card">
<h2><span class="step">2</span>What should the reference image give your picture?</h2>
<p class="hint">Pick the one job this recipe is for. The right preparation, safety policy, and tuning follow automatically.</p>
<div class="tiles" id="tiles"></div>
<div class="honesty" id="honesty"></div>
<label class="field" for="focus">Focus on one aspect <span style="opacity:.6">(optional)</span></label>
<input type="text" id="focus" maxlength="300" autocomplete="off"
placeholder='e.g. "the clothing and garment style, not the person" or "the weather and sky"'>
<div class="filehint">
Names WHAT the encoder studies from the image. Naming what to skip
works too ("&hellip;not the person or the background"). Strongest on
object-bound aspects like clothing; broad moods like weather mostly
ride the image itself.
</div>
</div>
<div class="card" id="loudcard">
<h2><span class="step">3</span>How strongly should it come through?</h2>
<p class="hint">This sets the recipe's response band. The card's strength slider still rides on top &mdash; these choices decide what that slider feels like.</p>
<div class="seg" id="loudseg"></div>
</div>
<div class="card">
<h2><span class="step">4</span>Fine-tune <span style="color:var(--muted);font-weight:400;font-size:13px">(optional &mdash; the defaults are the tested values)</span></h2>
<details id="advdetails">
<summary>Open fine-tuning</summary>
<div class="adv-row" id="subjectrow">
<input type="checkbox" id="subjectallow">
<label for="subjectallow" style="min-width:0">The reference's own subject may appear in the result</label>
</div>
<p class="adv-note" id="subjectnote"></p>
<div class="adv-row">
<label for="emphStruct">Layout influence (structure bands)</label>
<input type="range" id="emphStruct" min="0.3" max="1.5" step="0.05" value="1.0">
<output id="emphStructOut">1.0&times;</output>
</div>
<div class="adv-row">
<label for="emphFinish">Color &amp; finish influence (finish bands)</label>
<input type="range" id="emphFinish" min="0.5" max="1.5" step="0.05" value="1.0">
<output id="emphFinishOut">1.0&times;</output>
</div>
<p class="adv-note">
These scale the recipe's 12-band layer table exactly like the card's manual
<code>Structure layers pull</code> / <code>Finish layers pull</code> dials.
At 1.0&times; the tested family table is used as-is (and omitted from the file).
</p>
<div class="adv-row">
<label for="capOverride">Strength ceiling (cap)</label>
<input type="text" id="capOverride" inputmode="decimal" placeholder="auto" style="width:90px">
<span class="adv-note" style="margin:0">Hard limit the card can't exceed, whatever the slider says. Leave blank for the tested value.</span>
</div>
</details>
</div>
</div>
<div class="out">
<div class="card">
<h2>Your recipe file</h2>
<div class="tabs">
<button id="tabYaml" class="sel">YAML</button>
<button id="tabJson">JSON</button>
</div>
<pre id="code"></pre>
<div class="btns">
<button class="primary" id="btnDownload">Download</button>
<button id="btnCopy">Copy</button>
</div>
</div>
<div class="card">
<h2>Install it</h2>
<ol class="install">
<li>Save the file into the node pack's <code>custom_recipes/</code> folder
(or <code>&lt;ComfyUI user dir&gt;/krea_reference/recipes/</code>).</li>
<li>In ComfyUI, refresh node definitions (or restart).</li>
<li>Your recipe now appears in <code>Use image for</code> on the V10 guide card.</li>
</ol>
<p class="testbox" style="margin: 10px 0 0;">
Working examples to compare against ship in
<code>custom_recipes/starter-pack.yaml</code>: <em>borrow the
weather</em>, <em>borrow the clothing style</em> (a focus recipe),
and <em>cinematic color grade</em>.
</p>
</div>
<div class="card testbox">
<b>Two-minute check</b> &mdash; fix a seed and render at card strength
<code>0.4</code>, <code>0.65</code>, and <code>0.9</code>. You should get
quiet &rarr; clear &rarr; strong. If the reference's subject sneaks in,
untick "may appear" above; if nothing changes, pick a bolder loudness.
The stack's <code>prepared_references</code> output shows exactly what
the encoder studied.
</div>
</div>
</main>
<footer>
Values follow the pack's render-validated tables (see
<a href="https://github.com/kgilper/krea-reference/blob/main/custom_recipes/README.md">custom_recipes/README.md</a>
and the <a href="https://github.com/kgilper/krea-reference/blob/main/docs/krea-v10-user-guide.md#create-your-own-recipes">V10 guide</a>).
Full schema, layer math, and the manual derivation procedure live there for when you outgrow the builder.
</footer>
<script>
/*LOGIC-START*/
// ---- Data: render-validated base bundles (mirrors kg_krea_v10 recipes.py + starter pack, 2026-07-05/06 retunes) ----
var LAYER_TABLES = {
even: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
style: [0.25, 0.35, 0.45, 0.6, 0.8, 1.0, 1.0, 2.5, 5.0, 1.1, 4.0, 1.2],
styleTransfer: [0.062, 0.087, 0.113, 0.15, 0.2, 0.25, 1.25, 3.438, 6.875, 1.375, 5.5, 1.5],
palette: [0.15, 0.2, 0.3, 0.45, 0.7, 1.0, 1.0, 2.8, 5.5, 1.3, 4.5, 1.2],
material: [0.2, 0.3, 0.45, 0.65, 0.85, 1.0, 1.0, 2.0, 4.0, 1.2, 3.0, 1.1],
materialFinish: [0.05, 0.075, 0.113, 0.163, 0.213, 0.25, 1.25, 2.75, 5.5, 1.5, 4.125, 1.375],
lighting: [0.2, 0.25, 0.35, 0.5, 0.8, 1.0, 1.0, 2.2, 4.5, 1.4, 4.0, 1.2],
structureOnly: [1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25],
flat: [0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15]
};
// Each intent: artist wording + the validated bundle it starts from.
// "family" = appearance | structure | anchor | guard drives the loudness math.
var INTENTS = [
{ id: "palette", title: "Borrow its colors", blurb: "The palette and tonal feel, as soft color fields.",
family: "appearance", cant: "Its shapes and subject cannot arrive - the image is reduced to color blocks before the AI ever sees it.",
base: { role: "palette", treatment: "palette wash", color: 1.0, detail: 0.0, study: "256", framing: "stack",
subject: "avoid", early: 0.9, late: 0.9, guard: false, cap: 0.9, shape: 0.7, global: 1.8, table: "palette" } },
{ id: "style", title: "Borrow its overall look", blurb: "The medium, palette, and finish energy - real art direction.",
family: "appearance", cant: "Its subject and scene layout cannot arrive, and its deliberate pre-blur is de-selected so results stay crisp; brushwork lands as finish character, not a literal repaint.",
base: { role: "style", treatment: "strong blur", color: 1.0, detail: 0.3, study: "384", framing: "stack",
subject: "avoid", early: 0.85, late: 0.85, guard: false, cap: 0.65, shape: 0.85, global: 1.85,
table: "styleTransfer", forceLayers: true,
focus: "the artistic style: palette, medium, brushwork, art direction, and rendering finish - not the image's blurriness or soft focus, and not its subject or scene layout" } },
{ id: "lighting", title: "Borrow its lighting & mood", blurb: "Light color, contrast, and atmosphere as a scene-wide cast.",
family: "appearance", cant: "Its place and objects stay out - broad light and sky masses arrive as mood, so dramatic skies actually land.",
base: { role: "lighting", treatment: "strong blur", color: 1.0, detail: 0.15, study: "256", framing: "stack",
subject: "avoid", early: 1.0, late: 0.55, guard: false, cap: 1.25, shape: 0.8, global: 1.3,
table: "lighting",
focus: "the lighting: light direction, contrast, mood, color cast, glow, and shadow behavior - not the place, objects, or scene layout" } },
{ id: "material", title: "Borrow its surface feel", blurb: "Surface character - weave, glaze, relief energy - not just a recolor.",
family: "appearance", cant: "Exact grain or printed marks cannot arrive; near the top of the range a strong source can nudge your object's form - stay around 0.55 when the form must not move.",
base: { role: "material", treatment: "strong blur", color: 1.0, detail: 0.35, study: "384", framing: "stack",
subject: "avoid", early: 0.35, late: 0.9, guard: false, cap: 0.65, shape: 0.8, global: 1.55,
table: "materialFinish", forceLayers: true } },
{ id: "environment", title: "Borrow its setting's vibe", blurb: "The place's palette and room mood around your subject.",
family: "appearance", cant: "It sets the atmosphere; it does not paste the literal room behind your subject, and it cannot reshape your subject.",
base: { role: "environment", treatment: "palette wash", color: 1.0, detail: 0.3, study: "256", framing: "stack",
subject: "avoid", early: 1.1, late: 0.6, guard: false, cap: 1.2, shape: 0.7, global: 0.85, table: "style" } },
{ id: "mood", title: "Just a loose vibe", blurb: "Gentle mood-board inspiration that never dictates.",
family: "appearance", cant: "Specific content, layout, or details cannot arrive - this one is capped soft on purpose.",
base: { role: "loose", treatment: "palette wash", color: 1.0, detail: 0.2, study: "256", framing: "stack",
subject: "avoid", early: 0.8, late: 0.8, guard: false, cap: 0.9, shape: 0.65, global: 0.7, table: "style" } },
{ id: "composition", title: "Copy its arrangement", blurb: "Pose, placement, camera angle, and layout.",
family: "structure", cant: "Its colors and surface cannot arrive - it is studied as a grayscale blur.",
base: { role: "composition", treatment: "grayscale blur", color: 0.0, detail: 0.25, study: "stack", framing: "stack",
subject: "avoid", early: 1.2, late: 0.2, guard: false, cap: 1.25, shape: 1.3, global: 0.3, table: "even" } },
{ id: "framing", title: "Copy its camera framing", blurb: "Distance, crop, and viewpoint only.",
family: "structure", cant: "Subject, palette, and surface stay yours.",
base: { role: "framing", treatment: "grayscale blur", color: 0.0, detail: 0.15, study: "256", framing: "preserve aspect",
subject: "avoid", early: 1.2, late: 0.15, guard: false, cap: 1.2, shape: 0.95, global: 0.25, table: "even" } },
{ id: "shapes", title: "Copy its big shapes", blurb: "Silhouette and massing steer the composition.",
family: "structure", cant: "Color, texture, and small details cannot arrive - the structure-only tuning also keeps the recipe's gray study from draining your colors.",
base: { role: "shape only", treatment: "shape wash", color: 0.0, detail: 0.0, study: "256", framing: "stack",
subject: "avoid", early: 1.1, late: 0.0, guard: false, cap: 1.0, shape: 1.2, global: 0.05,
table: "structureOnly", forceLayers: true } },
{ id: "identity", title: "Keep its subject", blurb: "The person, product, or object travels into your new scene.",
family: "anchor", cant: "Note: it copies faithfully - printed markings too. Add an 'avoid copying text/logos' card if you don't want them.",
base: { role: "identity", treatment: "normal", color: 1.0, detail: 1.0, study: "stack", framing: "stack",
subject: "preserve", early: 1.0, late: 1.0, guard: false, cap: null, shape: 1.0, global: 1.0, table: "even" } },
{ id: "balanced", title: "A bit of everything", blurb: "Subject, palette, and layout each arrive a little.",
family: "anchor", cant: "Nothing is filtered out - use a more specific job when you want guarantees.",
base: { role: "balanced", treatment: "normal", color: 1.0, detail: 1.0, study: "stack", framing: "stack",
subject: "recipe", early: 1.0, late: 1.0, guard: false, cap: null, shape: 1.0, global: 1.0, table: "even" } },
{ id: "guard", title: "Block text & logos", blurb: "Use label-covered references safely - nothing readable copies.",
family: "guard", cant: "This card is clamped to a whisper (0.03) and rewrites the prompt toward blank surfaces; loudness does not apply.",
base: { role: "text/logo safe", treatment: "shape wash", color: 0.0, detail: 0.0, study: "256", framing: "stack",
subject: "avoid", early: 0.75, late: 0.0, guard: true, cap: 0.03, shape: 0.08, global: 0.0, table: "flat" } }
];
var LOUDNESS = [
{ id: "whisper", title: "Whisper", blurb: "A hint, even with the card turned up." },
{ id: "gentle", title: "Clear", blurb: "The tested default: lands clearly from card strength ≈0.55-0.65." },
{ id: "bold", title: "Bold", blurb: "Unmistakable, still subject-safe." }
];
// Labels reserved by the node (built-in dropdown labels + internal recipe
// keys), plus the shipped pack labels (custom_recipes/*.yaml load them
// automatically, and across recipe files the first definition of a label
// wins - so a new file reusing one would silently lose).
var RESERVED = [
"manual tuning", "balanced", "keep the same subject", "copy pose and layout",
"copy lighting and mood", "suggest the visual style", "suggest material or texture",
"copy big shapes only", "avoid copying text/logos", "suggest the color palette",
"use the background/setting", "copy the camera framing", "mood board only",
"identity", "composition", "lighting", "style gentle", "texture gentle",
"shape only", "text/logo safe", "palette only", "environment", "framing", "mood board",
"borrow the weather", "borrow the clothing style", "cinematic color grade",
"borrow drawing medium", "borrow photo finish",
"borrow the poster style", "borrow the soft media look", "borrow the pattern energy",
"borrow the era print look", "borrow the paper and canvas", "borrow the metallic accents",
"borrow the ornament borders", "borrow the stained glass look",
"style the finish only", "style the layout first",
"match the scene light", "match the monochrome look", "match the atmosphere",
"use the background only", "carry the subject over"
];
function round2(x) { return Math.round(x * 100) / 100; }
function clamp(x, lo, hi) { return Math.min(hi, Math.max(lo, x)); }
// Loudness scales shape within the render-calibrated bands and sets the cap.
function applyLoudness(base, family, loudness) {
var shape = base.shape, cap = base.cap;
if (family === "guard") return { shape: shape, cap: cap };
if (loudness === "whisper") {
shape = shape * 0.75;
cap = cap === null ? 0.8 : Math.min(cap, 0.6);
} else if (loudness === "bold") {
shape = shape * (family === "appearance" ? 1.25 : 1.15);
if (cap !== null) cap = Math.min(cap + 0.25, 1.5);
}
return { shape: clamp(round2(shape), 0, 3), cap: cap === null ? null : round2(clamp(cap, 0, 3)) };
}
// The card's Structure/Finish dial math over the family layer table.
function applyEmphasis(tableName, structMult, finishMult) {
var table = LAYER_TABLES[tableName];
return table.map(function (g, i) {
var m = i < 6 ? structMult : finishMult;
return Math.round(Math.min(g * m, 8.0) * 1000) / 1000;
});
}
// Build the final recipe object from the artist's choices.
function buildRecipe(opts) {
var intent = null;
for (var i = 0; i < INTENTS.length; i++) if (INTENTS[i].id === opts.intent) intent = INTENTS[i];
if (!intent) throw new Error("unknown intent " + opts.intent);
var b = intent.base;
var loud = applyLoudness(b, intent.family, intent.family === "guard" ? "gentle" : (opts.loudness || "gentle"));
var r = {
label: opts.label,
description: opts.description || "",
role: b.role, treatment: b.treatment, color: b.color, detail: b.detail,
study: b.study, framing: b.framing,
subject: (opts.subjectAllow && (intent.family === "appearance")) ? "allow" : b.subject,
early: b.early, late: b.late, guard: b.guard,
cap: (opts.capOverride !== undefined && opts.capOverride !== null && opts.capOverride !== "" && isFinite(parseFloat(opts.capOverride)))
? round2(clamp(parseFloat(opts.capOverride), 0, 3)) : loud.cap,
shape: loud.shape, global: b.global
};
// Optional semantic focus: what the encoder should study from the image
// ("the clothing and garment style, not the person"). Whitespace-normalized
// and length-capped to match the node's schema. Intents whose shipped
// recipe carries a focus (style, lighting) supply it as the default; the
// artist's own text replaces it.
var focus = (opts.focus || b.focus || "").split(/\s+/).join(" ").trim();
if (focus) r.focus = focus.slice(0, 300);
var sm = opts.emphStruct === undefined ? 1.0 : opts.emphStruct;
var fm = opts.emphFinish === undefined ? 1.0 : opts.emphFinish;
// Intents whose shipped recipe uses a table that differs from the role's
// default (style-transfer, material-finish, structure-only) always write
// `layers`, or the node would fall back to the role table.
if (sm !== 1.0 || fm !== 1.0 || b.forceLayers) r.layers = applyEmphasis(b.table, sm, fm);
return r;
}
function labelProblem(label) {
var t = (label || "").trim();
if (!t) return "Give the recipe a name.";
for (var i = 0; i < RESERVED.length; i++) {
if (RESERVED[i] === t) return '"' + t + '" is taken by a built-in choice - pick another name.';
}
return null;
}
function kebab(label) {
return (label || "recipe").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "recipe";
}
function yamlQuote(s) {
if (/[:#"'{}\[\],&*?|>%@`]|^\s|\s$|^$/.test(s)) return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
return s;
}
function toYaml(r) {
var lines = [];
lines.push("label: " + yamlQuote(r.label));
if (r.description) lines.push("description: " + yamlQuote(r.description));
lines.push("role: " + yamlQuote(r.role));
lines.push("treatment: " + yamlQuote(r.treatment));
lines.push("color: " + r.color);
lines.push("detail: " + r.detail);
lines.push('study: "' + r.study + '"');
lines.push("framing: " + yamlQuote(r.framing));
lines.push("subject: " + r.subject);
lines.push("early: " + r.early);
lines.push("late: " + r.late);
if (r.guard) lines.push("guard: true");
if (r.cap !== null && r.cap !== undefined) lines.push("cap: " + r.cap);
lines.push("shape: " + r.shape);
lines.push("global: " + r.global + " # no effect on Krea 2; kept for other models");
if (r.focus) lines.push("focus: " + yamlQuote(r.focus));
if (r.layers) lines.push("layers: [" + r.layers.join(", ") + "]");
return lines.join("\n") + "\n";
}
function toJson(r) {
var o = { label: r.label };
if (r.description) o.description = r.description;
o.role = r.role; o.treatment = r.treatment; o.color = r.color; o.detail = r.detail;
o.study = r.study; o.framing = r.framing; o.subject = r.subject;
o.early = r.early; o.late = r.late;
if (r.guard) o.guard = true;
if (r.cap !== null && r.cap !== undefined) o.cap = r.cap;
o.shape = r.shape; o["global"] = r.global;
if (r.focus) o.focus = r.focus;
if (r.layers) o.layers = r.layers;
return JSON.stringify(o, null, 2) + "\n";
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { INTENTS: INTENTS, LOUDNESS: LOUDNESS, RESERVED: RESERVED,
buildRecipe: buildRecipe, toYaml: toYaml, toJson: toJson, labelProblem: labelProblem, kebab: kebab };
}
/*LOGIC-END*/
// ---- UI wiring (browser only) ----
(function () {
if (typeof document === "undefined") return;
var state = { intent: "palette", loudness: "gentle", format: "yaml" };
var $ = function (id) { return document.getElementById(id); };
// intent tiles
var tilesEl = $("tiles");
INTENTS.forEach(function (it) {
var d = document.createElement("div");
d.className = "tile"; d.dataset.id = it.id; d.tabIndex = 0; d.setAttribute("role", "button");
d.innerHTML = "<b>" + it.title + "</b><span>" + it.blurb + "</span>";
function pick() { state.intent = it.id; render(); }
d.addEventListener("click", pick);
d.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(); } });
tilesEl.appendChild(d);
});
// loudness segment
var segEl = $("loudseg");
LOUDNESS.forEach(function (l) {
var b = document.createElement("button");
b.dataset.id = l.id;
b.innerHTML = "<b>" + l.title + "</b><span>" + l.blurb + "</span>";
b.addEventListener("click", function () { state.loudness = l.id; render(); });
segEl.appendChild(b);
});
["label", "desc", "capOverride", "focus"].forEach(function (id) { $(id).addEventListener("input", render); });
$("subjectallow").addEventListener("change", render);
[["emphStruct", "emphStructOut"], ["emphFinish", "emphFinishOut"]].forEach(function (pair) {
$(pair[0]).addEventListener("input", function () {
$(pair[1]).innerHTML = parseFloat($(pair[0]).value).toFixed(2).replace(/0$/, "") + "&times;";
render();
});
});
$("tabYaml").addEventListener("click", function () { state.format = "yaml"; render(); });
$("tabJson").addEventListener("click", function () { state.format = "json"; render(); });
function currentRecipe() {
return buildRecipe({
label: $("label").value.trim() || "my recipe",
description: $("desc").value.trim(),
intent: state.intent,
loudness: state.loudness,
focus: $("focus").value,
subjectAllow: $("subjectallow").checked,
emphStruct: parseFloat($("emphStruct").value),
emphFinish: parseFloat($("emphFinish").value),
capOverride: $("capOverride").value.trim()
});
}
function render() {
var intent = INTENTS.filter(function (i) { return i.id === state.intent; })[0];
// selections
Array.prototype.forEach.call(tilesEl.children, function (t) { t.classList.toggle("sel", t.dataset.id === state.intent); });
Array.prototype.forEach.call(segEl.children, function (b) { b.classList.toggle("sel", b.dataset.id === state.loudness); });
$("tabYaml").classList.toggle("sel", state.format === "yaml");
$("tabJson").classList.toggle("sel", state.format === "json");
// honesty strip
var h = $("honesty");
h.style.display = "block";
h.innerHTML = "<b>What this cannot do:</b> " + intent.cant;
// loudness applies?
$("loudcard").style.opacity = intent.family === "guard" ? 0.45 : 1;
Array.prototype.forEach.call(segEl.children, function (b) { b.disabled = intent.family === "guard"; });
// subject checkbox only where it means something
var subjRow = $("subjectrow");
var showSubj = intent.family === "appearance";
subjRow.style.display = showSubj ? "flex" : "none";
$("subjectnote").style.display = showSubj ? "block" : "none";
$("subjectnote").textContent = showSubj
? "Off (recommended): the recipe actively steers the reference's subject out. On: the model may include it."
: "";
// label check + filename
var problem = labelProblem($("label").value);
var err = $("labelerr");
if (problem && $("label").value.trim() !== "") { err.style.display = "block"; err.textContent = problem; }
else { err.style.display = "none"; }
var fname = kebab($("label").value) + (state.format === "yaml" ? ".yaml" : ".json");
$("filehint").textContent = "File name: custom_recipes/" + fname;
// output
var r = currentRecipe();
$("code").textContent = state.format === "yaml" ? toYaml(r) : toJson(r);
$("btnDownload").disabled = !!problem;
$("btnCopy").disabled = !!problem;
}
$("btnDownload").addEventListener("click", function () {
var text = $("code").textContent;
var fname = kebab($("label").value) + (state.format === "yaml" ? ".yaml" : ".json");
var blob = new Blob([text], { type: "text/plain" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.download = fname;
document.body.appendChild(a); a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 500);
});
$("btnCopy").addEventListener("click", function () {
var btn = $("btnCopy");
function done() { btn.textContent = "Copied!"; btn.classList.add("copied");
setTimeout(function () { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1400); }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText($("code").textContent).then(done);
} else {
var ta = document.createElement("textarea"); ta.value = $("code").textContent;
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); done(); } catch (e) {}
ta.remove();
}
});
render();
})();
</script>
</body>
</html>