Update the Ideogram 4 prompt generation/parsing/ui to handle the updated format notes better.

This commit is contained in:
Jaret Burkett
2026-06-16 09:44:38 -06:00
parent fcccc0fbd2
commit 86b19589a0
10 changed files with 497 additions and 155 deletions

View File

@@ -18,10 +18,47 @@ export function isIdeogramCaption(text: string): boolean {
}
}
// Normalize an arbitrary color string to a #rrggbb value usable by <input type=color>.
// Official medium tokens. `photograph` uses the `photo` style key; every other
// medium uses `art_style`. Users may also type a custom medium.
const MEDIUM_OPTIONS = ['photograph', 'illustration', '3d_render', 'painting', 'graphic_design'];
// Map old/variant medium spellings (e.g. our old "Illustration." / "3D render.")
// to a canonical token so the dropdown can match them; unknown values stay custom.
function canonMedium(m: string): string {
const key = (m || '').trim().replace(/\.+$/, '').trim().toLowerCase();
const aliases: Record<string, string> = {
photograph: 'photograph',
photo: 'photograph',
illustration: 'illustration',
'3d render': '3d_render',
'3d_render': '3d_render',
'3d-render': '3d_render',
'3drender': '3d_render',
render: '3d_render',
'3d': '3d_render',
painting: 'painting',
'graphic design': 'graphic_design',
graphic_design: 'graphic_design',
'graphic-design': 'graphic_design',
graphic: 'graphic_design',
};
return aliases[key] ?? (m || '').trim();
}
// Photograph branch uses `photo`; everything else uses `art_style`. An unknown
// (empty/custom) medium defaults to the photo branch.
function isPhotoMedium(m: string): boolean {
const c = canonMedium(m);
if (c === 'photograph') return true;
if (MEDIUM_OPTIONS.includes(c)) return false;
return true;
}
// Normalize a color to an UPPERCASE #RRGGBB value (expands #RGB). Returns
// '#000000' for anything unparseable (used to feed <input type=color>).
function toHex6(c: string): string {
const s = (c || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(s)) return s;
if (/^#[0-9a-fA-F]{6}$/.test(s)) return '#' + s.slice(1).toUpperCase();
if (/^#[0-9a-fA-F]{3}$/.test(s)) {
return (
'#' +
@@ -30,6 +67,7 @@ function toHex6(c: string): string {
.split('')
.map(ch => ch + ch)
.join('')
.toUpperCase()
);
}
return '#000000';
@@ -99,6 +137,49 @@ function TextAreaField({
);
}
// Medium picker: a dropdown of the official tokens plus a "Custom…" escape hatch
// that reveals a free-text input. Recognizes old/variant spellings via canonMedium.
function MediumField({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const canon = canonMedium(value);
const known = MEDIUM_OPTIONS.includes(canon);
const [custom, setCustom] = useState(!known && (value || '').trim() !== '');
const showCustom = custom || (!known && (value || '').trim() !== '');
return (
<label className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400">Medium</span>
<select
value={showCustom ? '__custom__' : canon}
onChange={e => {
const v = e.target.value;
if (v === '__custom__') setCustom(true);
else {
setCustom(false);
onChange(v);
}
}}
className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-100 outline-none focus:border-blue-500"
>
{MEDIUM_OPTIONS.map(m => (
<option key={m} value={m}>
{m}
</option>
))}
<option value="__custom__">Custom…</option>
</select>
{showCustom && (
<input
type="text"
value={value}
placeholder="custom medium"
spellCheck={false}
onChange={e => onChange(e.target.value)}
className="mt-1 bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-100 outline-none focus:border-blue-500"
/>
)}
</label>
);
}
function ColorPalette({ colors, max, onChange }: { colors: string[]; max: number; onChange: (c: string[]) => void }) {
const setAt = (i: number, v: string) => onChange(colors.map((c, idx) => (idx === i ? v : c)));
const removeAt = (i: number) => onChange(colors.filter((_, idx) => idx !== i));
@@ -109,7 +190,7 @@ function ColorPalette({ colors, max, onChange }: { colors: string[]; max: number
<input
type="color"
value={toHex6(c)}
onChange={e => setAt(i, e.target.value)}
onChange={e => setAt(i, toHex6(e.target.value))}
className="w-5 h-5 rounded cursor-pointer bg-transparent border-0 p-0"
title="Pick color"
/>
@@ -218,6 +299,40 @@ export default function IdeogramCaptionSidebar({
d.style_description = sd;
});
// Setting the medium may flip the branch (photograph ↔ non-photo). When it does,
// migrate the render text from photo↔art_style so we never keep both keys.
const setMedium = (value: string) =>
update(d => {
const sd = { ...(d.style_description || {}) };
const wasPhoto = isPhotoMedium(sd.medium ?? '');
const nowPhoto = isPhotoMedium(value);
sd.medium = value;
if (wasPhoto !== nowPhoto) {
const val = sd.photo ?? sd.art_style ?? '';
delete sd.photo;
delete sd.art_style;
if (nowPhoto) sd.photo = val;
else sd.art_style = val;
}
d.style_description = sd;
});
// The single render field writes to `photo` or `art_style` per the current branch.
const photoBranch = isPhotoMedium(style.medium ?? '');
const renderValue = (photoBranch ? style.photo : style.art_style) ?? style.photo ?? style.art_style ?? '';
const setRender = (value: string) =>
update(d => {
const sd = { ...(d.style_description || {}) };
if (isPhotoMedium(sd.medium ?? '')) {
sd.photo = value;
delete sd.art_style;
} else {
sd.art_style = value;
delete sd.photo;
}
d.style_description = sd;
});
const setElement = (i: number, mutator: (el: any) => void) =>
update(d => {
const els = d?.compositional_deconstruction?.elements;
@@ -279,12 +394,12 @@ export default function IdeogramCaptionSidebar({
<Section title="Style">
<TextField label="Aesthetics" value={style.aesthetics ?? ''} onChange={v => setStyle('aesthetics', v)} />
<TextField label="Lighting" value={style.lighting ?? ''} onChange={v => setStyle('lighting', v)} />
<TextField label="Photo / render" value={style.photo ?? ''} onChange={v => setStyle('photo', v)} />
<MediumField value={style.medium ?? ''} onChange={setMedium} />
<TextField
label="Medium"
value={style.medium ?? ''}
onChange={v => setStyle('medium', v)}
placeholder="Photograph."
label={photoBranch ? 'Photo (camera / film)' : 'Art style (rendering technique)'}
value={renderValue}
onChange={setRender}
placeholder={photoBranch ? '35mm film still, shallow depth of field' : 'flat vector, clean edges'}
/>
<div className="flex flex-col gap-1">
<span className="text-[10px] text-gray-400">Color palette (max 16)</span>

View File

@@ -417,7 +417,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Cinematic, tense, candid realism.",
"lighting": "Overcast afternoon daylight, soft and low-contrast, cool-neutral white balance.",
"photo": "35mm film still, subtle grain, natural depth of field.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#8C9B82", "#B7402A", "#5A5F57", "#9AA7AE", "#D98A3D"]
},
"compositional_deconstruction": {
@@ -466,7 +466,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Cozy, relaxed, intimate.",
"lighting": "Soft diffused window daylight, cool-neutral white balance, low contrast.",
"photo": "35mm film still, shallow depth of field, subtle grain.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#9A5A3E", "#E0D2BA", "#7C7872", "#B07C45", "#33312D"]
},
"compositional_deconstruction": {
@@ -536,7 +536,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "High-energy, surreal, neon nightlife.",
"lighting": "Dim club lighting with magenta and cyan washes and crisscrossing green and magenta laser beams cutting through haze.",
"photo": "Fish-eye lens with strong barrel distortion, deep shadow contrast.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#0B0B12", "#D81E8F", "#1FB6C9", "#37C46A", "#6A4A2E"]
},
"compositional_deconstruction": {
@@ -596,7 +596,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Bright, playful, candid.",
"lighting": "Bright overcast daylight, soft and shadowless, cool-neutral white balance.",
"photo": "35mm film still, natural depth of field, subtle grain.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#C9B68C", "#2E6B7A", "#9FB7BE", "#1B3A5C", "#E7E2D6"]
},
"compositional_deconstruction": {
@@ -646,7 +646,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Serene, rugged, wintry.",
"lighting": "Pale overcast winter daylight, even and shadowless, cool-neutral white balance.",
"photo": "35mm film still, subtle grain, soft natural focus.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#E8EDF0", "#6B4A30", "#3C5240", "#9AA6AD", "#C8A877"]
},
"compositional_deconstruction": {
@@ -705,7 +705,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Gritty, energetic, high-contrast.",
"lighting": "Dark stage lit by green and magenta laser beams through haze, deep shadow contrast, cool-neutral white balance.",
"photo": "35mm concert photograph, subtle grain, deep contrast.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#0C0C10", "#37C46A", "#D81E8F", "#C9C9C9", "#5A4633"]
},
"compositional_deconstruction": {
@@ -775,7 +775,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Rustic, focused, artisanal.",
"lighting": "Diffused overcast daylight from a high window, cool-neutral white balance, low contrast.",
"photo": "35mm film still, subtle grain, natural depth of field.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#8A5A3C", "#6E4327", "#9A9488", "#4A5340", "#C7B299"]
},
"compositional_deconstruction": {
@@ -845,7 +845,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Clean, minimal, editorial.",
"lighting": "Even diffused studio softbox lighting, neutral white balance, shadowless.",
"photo": "Studio fashion photograph, sharp focus, seamless white cyclorama.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#F2F2F0", "#9A9CA0", "#2A2F3C", "#5B5E66", "#D8D8D6"]
},
"compositional_deconstruction": {
@@ -874,7 +874,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Plain, candid, documentary.",
"lighting": "Overcast daylight, soft and even, cool-neutral white balance.",
"photo": "35mm film still, eye-level, subtle grain.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#9AA0A4", "#7C4A38", "#3A3D44", "#1E3A66", "#E8E6E0"]
},
"compositional_deconstruction": {
@@ -924,7 +924,7 @@ export const defaultIdeogramSamplesConfig: SampleConfig = {
"aesthetics": "Rugged, cinematic, post-apocalyptic.",
"lighting": "Pale dust-choked daylight softened by airborne grit, cool-neutral white balance, low contrast.",
"photo": "35mm film still, subtle grain, hazy distance.",
"medium": "Photograph.",
"medium": "photograph",
"color_palette": ["#C2A878", "#6B4A2E", "#3A352E", "#9A8A6C", "#B5562A"]
},
"compositional_deconstruction": {