/**
* @file
* @brief Emphasis stage: translate typed `((( )))` / `[[[ ]]]` emphasis into the active dialect.
*/
// The engine treats emphasis as a RENDERING FEATURE of its own syntax, not an AI-specific one:
// the author (or a DPL block) types nested `()` to emphasize and `[]` to de-emphasize a phrase,
// and this late stage renders that intent into whatever the target dialect understands — a numeric
// weight for Stable Diffusion / Midjourney, native nested braces for NovelAI, and a natural-language
// intensifier word for plain-text targets (ComfyUI, OpenAI, …) that have no weighting grammar.
//
// Each bracket level is ±10 intensity off a default of 50, capped at 5 levels: `(x)`=60 … `(((((x)))))`
// =100; `[x]`=40 … `[[[x]]]`=20 … 5 levels = 1 (floored — never 0). This COEXISTS with the per-keyword
// random roll (`randomEmphasis.js`): the roll decides which list keywords get bracketed; this stage is
// the single place that renders ALL bracket emphasis (typed or rolled) into the dialect form. It runs
// after the `{list}` / `{#gen}` expansion, so it also covers emphasis written inside DPL blocks (whose
// output is just prompt text by this point).
//
// What it deliberately leaves alone (passes through verbatim):
// - an explicit weight the author typed — `(phrase:1.2)` (the inner `:` opts out)
// - NovelAI alternation — `[a|b|c]` (the inner `|` opts out; it is a NovelAI-only render feature the
// engine does not own, so it bleeds through untouched)
// - a bare `[123]` numeric weight or a leftover `[i<10%]`-style dial token (digits / `%`)
// - asymmetric bracket runs (open count ≠ close count) — not a clean emphasis group
import { intensityWord } from "../dpl/dpl.js";
// One `(`/`[` per level = ±10 intensity; 5 levels reaches the 100 ceiling / the 1 floor.
const TYPED_MAX_LEVELS = 5;
const DEFAULT_INTENSITY = 50;
/** Bracket depth (≥1) → intensity percent. De-emphasis floors at 1 (never 0); emphasis caps at 100. */
function depthToIntensity(depth, lessEmphasis) {
const lvl = Math.min(depth, TYPED_MAX_LEVELS);
return lessEmphasis
? Math.max(1, DEFAULT_INTENSITY - 10 * lvl)
: Math.min(100, DEFAULT_INTENSITY + 10 * lvl);
}
/** A dialect weight number for an intensity (50 = ×1.0): trimmed to ≤2 decimals, no trailing zeros. */
function weightFor(intensity) {
return Number((intensity / DEFAULT_INTENSITY).toFixed(2)).toString();
}
/**
* Render one emphasis group into the active dialect.
* @param {string} mode The engine mode (`StableDiffusion` | `NovelAI` | `Midjourney` | other → plain).
* @param {string} phrase The inner text.
* @param {number} depth The bracket depth (≥1).
* @param {boolean} lessEmphasis De-emphasis (`[]`) rather than emphasis (`()`).
* @returns {string} The dialect-rendered phrase.
*/
function renderEmphasis(mode, phrase, depth, lessEmphasis) {
const intensity = depthToIntensity(depth, lessEmphasis);
const lvl = Math.min(depth, TYPED_MAX_LEVELS);
if (mode === "NovelAI") {
// NovelAI's native form: `{}` raises, `[]` lowers, one pair per ~5% step.
const [o, c] = lessEmphasis ? ["[", "]"] : ["{", "}"];
return o.repeat(lvl) + phrase + c.repeat(lvl);
}
if (mode === "Midjourney") return `${phrase}::${weightFor(intensity)}`;
if (mode === "StableDiffusion") return `(${phrase}:${weightFor(intensity)})`;
// Plain / natural language (ComfyUI, OpenAI, …): a leading intensity word, no syntax.
return `${intensityWord(intensity)} ${phrase}`;
}
// A balanced, symmetric run of N opening brackets, an inner with none of the bracket/`|`/`:` chars,
// then N closing brackets. The `|`/`:` exclusions are what let `(x:1.2)` weights and `[a|b]` NovelAI
// alternation pass through untouched (they simply don't match).
// The `(?=(\(+))\1` prefix captures the opening run in a lookahead then re-consumes it via the
// backreference — an atomic-group emulation that stops the `+` from backtracking (JS has no atomic
// groups), so a long run of unmatched brackets can't cause super-linear runtime. Capture groups are
// unchanged: 1 = opening run, 2 = inner, 3 = closing run.
const PAREN_RE = /(?=(\(+))\1([^()|:]+?)(\)+)/g;
const BRACK_RE = /(?=(\[+))\1([^[\]|:]+?)(\]+)/g;
/** Replace every clean emphasis group via `re` (one capture: open run, inner, close run). */
function convert(text, re, mode, lessEmphasis) {
return text.replace(re, (m, open, inner, close) => {
if (open.length !== close.length) return m; // asymmetric → not an emphasis group
const phrase = inner.trim();
if (phrase === "") return m;
// De-emphasis brackets must not swallow a bare `[123]` weight or a `%`-bearing dial leftover.
if (lessEmphasis && (/^\d+$/.test(phrase) || phrase.includes("%"))) return m;
return renderEmphasis(mode, phrase, open.length, lessEmphasis);
});
}
/**
* The emphasis stage: render typed `()` / `[]` emphasis into the active dialect.
* @param {string} prompt The (already list/gen-expanded) prompt text.
* @param {object} settings The merged settings (reads `settings.mode`).
* @returns {string} The prompt with emphasis rendered for the dialect.
*/
export default function emphasis(prompt, settings = {}) {
const mode = settings.mode || "StableDiffusion";
let out = convert(prompt, PAREN_RE, mode, false);
out = convert(out, BRACK_RE, mode, true);
return out;
}