Update the Ideogram 4 prompt generation/parsing/ui to handle the updated format notes better.
This commit is contained in:
parent
fcccc0fbd2
commit
86b19589a0
|
|
@ -8,6 +8,7 @@ from PIL import Image
|
|||
|
||||
from .Qwen3VLCaptioner import Qwen3VLCaptioner
|
||||
from .prompts.ideogram4_caption_prompt import ideogram4_caption_prompt
|
||||
from toolkit.ideogram_caption import normalize_caption_dict
|
||||
import transformers
|
||||
import logging
|
||||
import warnings
|
||||
|
|
@ -25,11 +26,6 @@ MIN_NEW_TOKENS = 3072
|
|||
# generator was trained on, instead of ugly fractions like 1023:768.
|
||||
MAX_AR_DENOMINATOR = 16
|
||||
|
||||
# color_palette caps: the model often ignores these, so we enforce them.
|
||||
MAX_IMAGE_PALETTE = 16 # style_description.color_palette
|
||||
MAX_ELEMENT_PALETTE = 5 # per-element color_palette
|
||||
HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
||||
|
||||
|
||||
class Ideogram4Captioner(Qwen3VLCaptioner):
|
||||
def __init__(self, process_id: int, job, config: OrderedDict, **kwargs):
|
||||
|
|
@ -109,65 +105,23 @@ class Ideogram4Captioner(Qwen3VLCaptioner):
|
|||
# stored order is [y1, x1, y2, x2]
|
||||
return [y1, x1, y2, x2]
|
||||
|
||||
def _sanitize_palette(self, palette, max_len):
|
||||
"""Keep unique, valid hex colors in order, capped to max_len. Returns the
|
||||
cleaned list, or None if nothing valid remains (drop the key)."""
|
||||
if not isinstance(palette, (list, tuple)):
|
||||
return None
|
||||
seen = set()
|
||||
out = []
|
||||
for c in palette:
|
||||
if not isinstance(c, str):
|
||||
continue
|
||||
c = c.strip()
|
||||
if not HEX_COLOR_RE.match(c):
|
||||
continue
|
||||
key = c.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(c)
|
||||
if len(out) >= max_len:
|
||||
break
|
||||
return out or None
|
||||
|
||||
def _normalize_caption(self, data: dict) -> dict:
|
||||
"""Validate/cleanup the parsed caption before storage: drop input-only
|
||||
aspect_ratio, reorder bboxes to [y1,x1,y2,x2], and cap color palettes
|
||||
(16 per image, 5 per element) since the model often exceeds them."""
|
||||
# aspect_ratio is input-only context, not part of the output. Drop it if
|
||||
# the model echoed it anyway.
|
||||
data.pop("aspect_ratio", None)
|
||||
|
||||
style = data.get("style_description")
|
||||
if isinstance(style, dict) and "color_palette" in style:
|
||||
pal = self._sanitize_palette(style["color_palette"], MAX_IMAGE_PALETTE)
|
||||
if pal is None:
|
||||
style.pop("color_palette", None)
|
||||
else:
|
||||
style["color_palette"] = pal
|
||||
|
||||
"""Cleanup the parsed caption before storage. The model emits bboxes in
|
||||
[x1,y1,x2,y2]; convert each to our stored [y1,x1,y2,x2] order, then hand off
|
||||
to the shared normalizer for the rest: drop aspect_ratio, enforce the
|
||||
photo/art_style branch and key order, canonicalize medium, and cap/uppercase
|
||||
color palettes (16 per image, 5 per element)."""
|
||||
decon = data.get("compositional_deconstruction", {})
|
||||
elements = decon.get("elements", [])
|
||||
elements = decon.get("elements", []) if isinstance(decon, dict) else []
|
||||
if isinstance(elements, list):
|
||||
for el in elements:
|
||||
if not isinstance(el, dict):
|
||||
continue
|
||||
if "bbox" in el:
|
||||
if isinstance(el, dict) and "bbox" in el:
|
||||
cleaned = self._convert_bbox(el["bbox"])
|
||||
if cleaned is None:
|
||||
el.pop("bbox", None)
|
||||
else:
|
||||
el["bbox"] = cleaned
|
||||
if "color_palette" in el:
|
||||
pal = self._sanitize_palette(
|
||||
el["color_palette"], MAX_ELEMENT_PALETTE
|
||||
)
|
||||
if pal is None:
|
||||
el.pop("color_palette", None)
|
||||
else:
|
||||
el["color_palette"] = pal
|
||||
return data
|
||||
return normalize_caption_dict(data)
|
||||
|
||||
def get_caption_for_file(self, file_path: str) -> Optional[str]:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ You are CAPTIONING a real image, not imagining one. Describe ONLY what is visibl
|
|||
## OUTPUT CONTRACT — exactly three top-level keys, in this order:
|
||||
|
||||
```json
|
||||
{"high_level_description":"...","style_description":{"aesthetics":"...","lighting":"...","photo":"...","medium":"...","color_palette":["#RRGGBB"]},"compositional_deconstruction":{"background":"...","elements":[ ... ]}}
|
||||
{"high_level_description":"...","style_description":{ ...see STYLE DESCRIPTION... },"compositional_deconstruction":{"background":"...","elements":[ ... ]}}
|
||||
```
|
||||
|
||||
- Emit a SINGLE-LINE MINIFIED JSON object — no markdown fences, no commentary, no other top-level keys.
|
||||
|
|
@ -44,22 +44,34 @@ BAD (over-specifies): `A male soccer player captured mid-kick on a bright green
|
|||
|
||||
## STYLE DESCRIPTION — the `style_description` block (always required)
|
||||
|
||||
A nested object capturing the image's overall look, OBSERVED from the image (never invented). Exactly these five keys:
|
||||
- `aesthetics` — the overall mood/aesthetic in a short phrase (`Cinematic, minimal, serene.` / `Bright, playful, high-energy.`).
|
||||
A nested object capturing the image's overall look, OBSERVED from the image (never invented). It carries EXACTLY ONE render key — `photo` for photographs, `art_style` for everything else (illustration / 3D render / painting / graphic design) — NEVER both. The key order is strict and depends on the branch:
|
||||
|
||||
- **Photograph** → keys in this order: `aesthetics`, `lighting`, `photo`, `medium`, `color_palette`
|
||||
```json
|
||||
{"aesthetics":"...","lighting":"...","photo":"...","medium":"photograph","color_palette":["#RRGGBB"]}
|
||||
```
|
||||
- **Non-photo** (illustration / 3D / painting / graphic design) → keys in this order: `aesthetics`, `lighting`, `medium`, `art_style`, `color_palette`
|
||||
```json
|
||||
{"aesthetics":"...","lighting":"...","medium":"illustration","art_style":"...","color_palette":["#RRGGBB"]}
|
||||
```
|
||||
|
||||
Field meanings:
|
||||
- `aesthetics` — the overall mood/aesthetic in a short phrase (`cinematic, minimal, serene` / `bright, playful, high-energy`).
|
||||
- `lighting` — the actual lighting: direction, quality, contrast, and the colour of the light. Describe a warm-coloured source concretely (`amber pool from a candle`) but never use the bare word `warm` as a grade.
|
||||
- `photo` — the medium-specific capture/render spec. Photograph → camera/film look, framing, grain, focus (`35mm film still, 16:9 framing, subtle grain, shallow depth of field`). Other media → the rendering technique (`flat vector, clean edges` / `octane 3D render, soft global illumination` / `loose watercolor on textured paper`).
|
||||
- `medium` — one short phrase: `Photograph.` / `Illustration.` / `3D render.` / `Graphic design.` Read it from the image; do not impose a default.
|
||||
- `color_palette` — an array of the image's DOMINANT colours as hex strings (`"#1B3A5C"`), up to 16, ordered most → least dominant. Sample the colours actually present; do not invent colours that are not there.
|
||||
- `photo` (photographs ONLY) — the camera/film capture spec: framing, grain, focus (`35mm film still, 16:9 framing, subtle grain, shallow depth of field`).
|
||||
- `art_style` (non-photo ONLY) — the rendering technique (`flat vector, clean edges` / `octane 3D render, soft global illumination` / `loose watercolor on textured paper`).
|
||||
- `medium` — exactly one token: `photograph` / `illustration` / `3d_render` / `painting` / `graphic_design`. Read it from the image; do not impose a default. Photograph ⇒ use `photo`; any other ⇒ use `art_style`.
|
||||
- `color_palette` — an array of the image's DOMINANT colours as UPPERCASE `#RRGGBB` hex strings (`"#1B3A5C"`), up to 16, ordered most → least dominant. Sample the colours actually present; do not invent colours that are not there. ALWAYS the last key.
|
||||
|
||||
## ELEMENTS — what they are, what they're not
|
||||
|
||||
Each element is one of:
|
||||
Each element is one of (keys in EXACTLY this order):
|
||||
```
|
||||
{"type":"obj","bbox":[x1,y1,x2,y2],"color_palette":["#RRGGBB"],"desc":"..."}
|
||||
{"type":"text","bbox":[x1,y1,x2,y2],"color_palette":["#RRGGBB"],"text":"LINE ONE\\nLINE TWO","desc":"..."}
|
||||
{"type":"obj","bbox":[x1,y1,x2,y2],"desc":"...","color_palette":["#RRGGBB"]}
|
||||
{"type":"text","bbox":[x1,y1,x2,y2],"text":"LINE ONE\\nLINE TWO","desc":"...","color_palette":["#RRGGBB"]}
|
||||
```
|
||||
|
||||
`bbox` and `color_palette` are both OPTIONAL per-element. `bbox`: see BBOX section below. `color_palette`: up to 5 hex strings of that element's own dominant colours — include it when the element has distinctive colours worth pinning (a red jacket, a brand logo, coloured text), omit it for colour-neutral elements.
|
||||
`bbox` and `color_palette` are both OPTIONAL per-element; when present they keep the order shown above (`color_palette` is always LAST). `bbox`: see BBOX section below. `color_palette`: up to 5 UPPERCASE `#RRGGBB` strings of that element's own dominant colours — include it when the element has distinctive colours worth pinning (a red jacket, a brand logo, coloured text), omit it for colour-neutral elements.
|
||||
|
||||
### SINGLE SUBJECT = SINGLE ELEMENT
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ You convert a natural-language user idea into a structured JSON caption an image
|
|||
## OUTPUT CONTRACT — exactly three top-level keys, in this order:
|
||||
|
||||
```json
|
||||
{"high_level_description":"...","style_description":{"aesthetics":"...","lighting":"...","photo":"...","medium":"...","color_palette":["#RRGGBB"]},"compositional_deconstruction":{"background":"...","elements":[ ... ]}}
|
||||
{"high_level_description":"...","style_description":{ ...see style_description... },"compositional_deconstruction":{"background":"...","elements":[ ... ]}}
|
||||
```
|
||||
|
||||
- Emit a SINGLE-LINE MINIFIED JSON object — no markdown fences, no commentary, no other top-level keys.
|
||||
|
|
@ -35,24 +35,29 @@ BAD (over-specifies): `A male soccer player captured mid-kick on a bright green
|
|||
|
||||
### `style_description` — the global look block (always required)
|
||||
|
||||
A nested object with exactly these five keys:
|
||||
- `aesthetics` — overall mood/aesthetic in a short phrase (`Cinematic, minimal, serene.`).
|
||||
A nested object carrying EXACTLY ONE render key — `photo` for photographs, `art_style` for everything else — NEVER both. Key order is strict and branch-dependent:
|
||||
|
||||
- **Photograph** → `aesthetics`, `lighting`, `photo`, `medium`, `color_palette`
|
||||
- **Non-photo** (illustration / 3D / painting / graphic design) → `aesthetics`, `lighting`, `medium`, `art_style`, `color_palette`
|
||||
|
||||
- `aesthetics` — overall mood/aesthetic in a short phrase (`cinematic, minimal, serene`).
|
||||
- `lighting` — direction, quality, contrast, and colour of the light. Describe a warm-coloured source concretely (`amber sun low at the horizon`); never use the bare word `warm` as a grade.
|
||||
- `photo` — the medium-specific capture/render spec. Photograph → camera/film look, framing, grain, focus (`35mm motion-picture film still, 16:9 framing, subtle grain`). Other media → the rendering technique (`flat vector, clean edges`; `octane 3D render`; `loose watercolor on textured paper`).
|
||||
- `medium` — one short phrase: `Photograph.` / `Illustration.` / `3D render.` / `Graphic design.`
|
||||
- `color_palette` — an array of the dominant colours as hex strings (`"#1B3A5C"`), up to 16, ordered most → least dominant. This conditions the image's colours directly, so commit to the actual hexes you intend.
|
||||
- `photo` (photographs ONLY) — the camera/film capture spec: framing, grain, focus (`35mm motion-picture film still, 16:9 framing, subtle grain`).
|
||||
- `art_style` (non-photo ONLY) — the rendering technique (`flat vector, clean edges`; `octane 3D render`; `loose watercolor on textured paper`).
|
||||
- `medium` — exactly one token: `photograph` / `illustration` / `3d_render` / `painting` / `graphic_design`. Photograph ⇒ use `photo`; any other ⇒ use `art_style`.
|
||||
- `color_palette` — an array of the dominant colours as UPPERCASE `#RRGGBB` hex strings (`"#1B3A5C"`), up to 16, ordered most → least dominant. This conditions the image's colours directly, so commit to the actual hexes you intend. ALWAYS the last key.
|
||||
|
||||
Name a recognized style ONCE here (see PLANNING → Style commitment); do not append invented technique detail on top of a well-known style name.
|
||||
|
||||
## ELEMENTS — what they are, what they're not
|
||||
|
||||
Each element is one of:
|
||||
Each element is one of (keys in EXACTLY this order):
|
||||
```
|
||||
{"type":"obj","bbox":[y1,x1,y2,x2],"color_palette":["#RRGGBB"],"desc":"..."}
|
||||
{"type":"text","bbox":[y1,x1,y2,x2],"color_palette":["#RRGGBB"],"text":"LINE ONE\nLINE TWO","desc":"..."}
|
||||
{"type":"obj","bbox":[y1,x1,y2,x2],"desc":"...","color_palette":["#RRGGBB"]}
|
||||
{"type":"text","bbox":[y1,x1,y2,x2],"text":"LINE ONE\nLINE TWO","desc":"...","color_palette":["#RRGGBB"]}
|
||||
```
|
||||
|
||||
`bbox` and `color_palette` are both OPTIONAL per-element. `bbox`: see BBOX section below. `color_palette`: up to 5 hex strings steering that element's own dominant colours — include it when the element has a distinctive colour (a red jacket, a brand logo, coloured text), omit it otherwise.
|
||||
`bbox` and `color_palette` are both OPTIONAL per-element; when present they keep the order shown (`color_palette` is always LAST). `bbox`: see BBOX section below. `color_palette`: up to 5 UPPERCASE `#RRGGBB` strings steering that element's own dominant colours — include it when the element has a distinctive colour (a red jacket, a brand logo, coloured text), omit it otherwise.
|
||||
|
||||
### SINGLE SUBJECT = SINGLE ELEMENT
|
||||
|
||||
|
|
@ -224,13 +229,14 @@ The "dense unenumerable group" exception (crowd of thousands, field of wildflowe
|
|||
|
||||
### 1. Pick a medium
|
||||
|
||||
`photograph | illustration | 3D render | graphic design` — applies as natural-language framing inside HLD/background, NOT as a structured slot.
|
||||
`photograph | illustration | 3d_render | painting | graphic_design` — this is the `medium` token (photograph ⇒ `photo`, all others ⇒ `art_style`), and it also frames HLD/background prose naturally.
|
||||
|
||||
Decision: **DESIGNED artifact vs CAPTURED / DRAWN / RENDERED moment.**
|
||||
- **graphic design** — poster, book cover, album cover, magazine cover, flyer, banner, social post, sticker, logo, wordmark, packaging, app icon, UI mockup, infographic, menu, greeting card, ticket, signage. If a human designer would sit at a desk to make it.
|
||||
- **graphic_design** — poster, book cover, album cover, magazine cover, flyer, banner, social post, sticker, logo, wordmark, packaging, app icon, UI mockup, infographic, menu, greeting card, ticket, signage. If a human designer would sit at a desk to make it.
|
||||
- **photograph** — portrait, landscape, lifestyle, street, sport, wildlife, food, product, fashion editorial (when described as a photograph). Default for ambiguous everyday scenes.
|
||||
- **illustration** — cartoon, anime, manga, comic, watercolor, oil painting, ink, vector, pixel art, children's book illustration, named studios (Ghibli, KyoAni, Pixar 2D).
|
||||
- **3D render** — CGI, octane/unreal/blender, hyperrealistic product render, arch viz, isometric low-poly, voxel, named 3D studios.
|
||||
- **illustration** — cartoon, anime, manga, comic, ink, vector, pixel art, children's book illustration, named studios (Ghibli, KyoAni, Pixar 2D).
|
||||
- **painting** — watercolor, oil, gouache, acrylic, traditional painterly work.
|
||||
- **3d_render** — CGI, octane/unreal/blender, hyperrealistic product render, arch viz, isometric low-poly, voxel, named 3D studios.
|
||||
|
||||
Silent / ambiguous → photograph (default). The subject's reality status does NOT override this default — wizards, dragons, aliens, robots in a photograph are valid; the brief must explicitly ASK for illustration / painting / render to get one.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ You convert a user prompt into a structured JSON caption an image renderer can c
|
|||
## OUTPUT CONTRACT — exactly three top-level keys, in this order:
|
||||
|
||||
```json
|
||||
{"high_level_description":"...","style_description":{"aesthetics":"...","lighting":"...","photo":"...","medium":"...","color_palette":["#RRGGBB"]},"compositional_deconstruction":{"background":"...","elements":[ ... ]}}
|
||||
{"high_level_description":"...","style_description":{ ...see STYLE DESCRIPTION... },"compositional_deconstruction":{"background":"...","elements":[ ... ]}}
|
||||
```
|
||||
|
||||
- Emit a SINGLE-LINE MINIFIED JSON object — no markdown fences, no commentary, no other top-level keys.
|
||||
|
|
@ -34,23 +34,29 @@ One short sentence, reads like a natural prompt, starts with the subject — no
|
|||
|
||||
## STYLE DESCRIPTION — the `style_description` block (always required)
|
||||
|
||||
A nested object with exactly these five keys, filled FROM the prompt:
|
||||
A nested object, filled FROM the prompt. It carries EXACTLY ONE render key — `photo` for photographs, `art_style` for everything else — NEVER both. Key order is strict and branch-dependent:
|
||||
|
||||
- **Photograph** → `aesthetics`, `lighting`, `photo`, `medium`, `color_palette`
|
||||
- **Non-photo** (illustration / 3D / painting / graphic design) → `aesthetics`, `lighting`, `medium`, `art_style`, `color_palette`
|
||||
|
||||
Fields:
|
||||
- `aesthetics` — the overall mood/aesthetic in a short phrase.
|
||||
- `lighting` — the lighting (direction, quality, colour). Describe a warm-coloured source concretely; never use the bare word `warm` as a grade.
|
||||
- `photo` — the medium-specific capture/render spec (photograph → camera/film look, framing, grain, focus; other media → the rendering technique).
|
||||
- `medium` — one short phrase: `Photograph.` / `Illustration.` / `3D render.` / `Graphic design.`
|
||||
- `color_palette` — an array of dominant colours as hex strings (`"#1B3A5C"`), up to 16, ordered most → least dominant.
|
||||
- `photo` (photographs ONLY) — the camera/film capture spec (framing, grain, focus).
|
||||
- `art_style` (non-photo ONLY) — the rendering technique (`flat vector, clean edges`; `octane 3D render`; `loose watercolor`).
|
||||
- `medium` — exactly one token: `photograph` / `illustration` / `3d_render` / `painting` / `graphic_design`. Photograph ⇒ use `photo`; any other ⇒ use `art_style`.
|
||||
- `color_palette` — an array of dominant colours as UPPERCASE `#RRGGBB` strings (`"#1B3A5C"`), up to 16, ordered most → least dominant. ALWAYS the last key.
|
||||
|
||||
Respect FIDELITY: if the prompt NAMES a style, medium, artist, or look, put it in these fields BY NAME (e.g. `medium`/`photo`/`aesthetics`) and do NOT invent its characteristics. Pull lighting and colours from what the prompt states. In faithful mode, only commit to a value the prompt implies, keeping the rest minimal; in creative mode you may infer fitting style values — but never elaborate a named style and never override what the user gave.
|
||||
Respect FIDELITY: if the prompt NAMES a style, medium, artist, or look, put it in these fields BY NAME (e.g. `medium`/`art_style`/`aesthetics`) and do NOT invent its characteristics. Pull lighting and colours from what the prompt states. In faithful mode, only commit to a value the prompt implies, keeping the rest minimal; in creative mode you may infer fitting style values — but never elaborate a named style and never override what the user gave.
|
||||
|
||||
## ELEMENTS
|
||||
|
||||
Each element is one of:
|
||||
Each element is one of (keys in EXACTLY this order):
|
||||
```
|
||||
{"type":"obj","bbox":[y1,x1,y2,x2],"color_palette":["#RRGGBB"],"desc":"..."}
|
||||
{"type":"text","bbox":[y1,x1,y2,x2],"color_palette":["#RRGGBB"],"text":"LINE ONE\nLINE TWO","desc":"..."}
|
||||
{"type":"obj","bbox":[y1,x1,y2,x2],"desc":"...","color_palette":["#RRGGBB"]}
|
||||
{"type":"text","bbox":[y1,x1,y2,x2],"text":"LINE ONE\nLINE TWO","desc":"...","color_palette":["#RRGGBB"]}
|
||||
```
|
||||
`bbox` and `color_palette` are both OPTIONAL per element. `bbox`: see BBOX. `color_palette`: up to 5 hex strings of that element's dominant colours — include it when the prompt gives the element a distinctive colour (a red jacket, coloured text), otherwise omit.
|
||||
`bbox` and `color_palette` are both OPTIONAL per element; when present they keep the order shown (`color_palette` is always LAST). `bbox`: see BBOX. `color_palette`: up to 5 UPPERCASE `#RRGGBB` strings of that element's dominant colours — include it when the prompt gives the element a distinctive colour (a red jacket, coloured text), otherwise omit.
|
||||
|
||||
- **One coherent subject = ONE element.** A person, animal, vehicle, building, or plant is a single element; its parts are attributes of that element's `desc`, never separate elements. Multiple distinct subjects = multiple elements (one each).
|
||||
- **`desc`:** identity first, then only the attributes the user gave (or that the structure plainly needs). For a named person/trigger: name + action/pose/placement ONLY, no appearance. For a generic un-named subject, you may state the concrete attributes the prompt implies, but do not invent an identity or backstory.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from toolkit.models.base_model import BaseModel
|
|||
from toolkit.basic import flush
|
||||
from toolkit.print import print_acc
|
||||
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||
from toolkit.ideogram_caption import digest_caption_string
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
|
|
@ -419,6 +420,10 @@ class Ideogram4Model(BaseModel):
|
|||
# length -- important for the long structured (JSON) captions.
|
||||
features_list = []
|
||||
for p in prompt:
|
||||
# Digest the prompt: migrate any old-format Ideogram caption into the
|
||||
# current schema and serialize it compact (the form the renderer wants).
|
||||
# Plain-text prompts pass straight through unchanged.
|
||||
p = digest_caption_string(p)
|
||||
messages = [{"role": "user", "content": [{"type": "text", "text": p}]}]
|
||||
text = self.tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True, tokenize=False
|
||||
|
|
|
|||
|
|
@ -0,0 +1,282 @@
|
|||
"""Shared helpers for Ideogram-4 structured JSON captions.
|
||||
|
||||
This is the single source of truth for the caption schema so the captioner, the
|
||||
prompt upsampler, the dataloader, and the model encoder all agree. It encodes the
|
||||
official Ideogram-4 rules and, crucially, MIGRATES the old caption format we used
|
||||
before those rules were published into the new one ("digest" old, emit new).
|
||||
|
||||
Official schema (summary):
|
||||
- three top-level keys: high_level_description (optional), style_description
|
||||
(optional), compositional_deconstruction (required).
|
||||
- style_description holds EXACTLY ONE of `photo` (photographs) or `art_style`
|
||||
(illustration/painting/3D/graphic design), never both. Key order is strict and
|
||||
branch-dependent:
|
||||
photo branch: aesthetics, lighting, photo, medium, color_palette
|
||||
non-photo branch: aesthetics, lighting, medium, art_style, color_palette
|
||||
- medium is one of: photograph, illustration, 3d_render, painting, graphic_design
|
||||
- color_palette: UPPERCASE #RRGGBB only, up to 16 per image / 5 per element.
|
||||
- elements, strict key order:
|
||||
obj: type, bbox, desc, color_palette
|
||||
text: type, bbox, text, desc, color_palette
|
||||
bbox is optional, normalized 0-1000, [y_min, x_min, y_max, x_max], top-left.
|
||||
- serialize compact: separators=(",", ":"), ensure_ascii=False (no \\uXXXX).
|
||||
|
||||
The OLD format we previously emitted differed by: always using `photo` (even for
|
||||
non-photo media), putting `color_palette` before `desc`/`text`, title-cased medium
|
||||
with a trailing period ("Illustration."), and lowercase / 3-digit hex. Every
|
||||
function here accepts the old shape and returns the new one.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
MAX_IMAGE_PALETTE = 16 # style_description.color_palette
|
||||
MAX_ELEMENT_PALETTE = 5 # per-element color_palette
|
||||
|
||||
# Canonical medium tokens (official set).
|
||||
MEDIUM_OPTIONS = [
|
||||
"photograph",
|
||||
"illustration",
|
||||
"3d_render",
|
||||
"painting",
|
||||
"graphic_design",
|
||||
]
|
||||
|
||||
# Map common variants (including our old "Title." style) to the canonical token.
|
||||
# Anything not listed is treated as a custom medium and preserved verbatim.
|
||||
_MEDIUM_ALIASES = {
|
||||
"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",
|
||||
}
|
||||
|
||||
_HEX6_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
||||
_HEX3_RE = re.compile(r"^#[0-9a-fA-F]{3}$")
|
||||
|
||||
|
||||
def canon_medium(medium):
|
||||
"""Canonicalize a medium string to an official token when recognized,
|
||||
otherwise return it stripped (custom mediums are allowed, preserved as-is)."""
|
||||
if not isinstance(medium, str):
|
||||
return medium
|
||||
key = medium.strip().rstrip(".").strip().lower()
|
||||
if key in _MEDIUM_ALIASES:
|
||||
return _MEDIUM_ALIASES[key]
|
||||
return medium.strip()
|
||||
|
||||
|
||||
def is_photo_medium(medium):
|
||||
"""True for the photograph branch (uses `photo`), False for the art_style branch."""
|
||||
return canon_medium(medium) == "photograph"
|
||||
|
||||
|
||||
def normalize_hex(color):
|
||||
"""Return an UPPERCASE #RRGGBB string, expanding #RGB -> #RRGGBB. None if invalid."""
|
||||
if not isinstance(color, str):
|
||||
return None
|
||||
s = color.strip()
|
||||
if _HEX6_RE.match(s):
|
||||
return "#" + s[1:].upper()
|
||||
if _HEX3_RE.match(s):
|
||||
return "#" + "".join(ch * 2 for ch in s[1:]).upper()
|
||||
return None
|
||||
|
||||
|
||||
def sanitize_palette(palette, max_len):
|
||||
"""Keep unique, valid, UPPERCASE hex colors in order, capped to max_len.
|
||||
Returns the cleaned list, or None if nothing valid remains (drop the key)."""
|
||||
if not isinstance(palette, (list, tuple)):
|
||||
return None
|
||||
seen = set()
|
||||
out = []
|
||||
for c in palette:
|
||||
h = normalize_hex(c)
|
||||
if h is None or h in seen:
|
||||
continue
|
||||
seen.add(h)
|
||||
out.append(h)
|
||||
if len(out) >= max_len:
|
||||
break
|
||||
return out or None
|
||||
|
||||
|
||||
def normalize_style(style):
|
||||
"""Reorder/clean style_description into the correct branch (photo vs art_style)
|
||||
with the strict key order, canonical medium, and uppercase palette. Accepts the
|
||||
old shape (always `photo`) and migrates it based on the medium."""
|
||||
if not isinstance(style, dict):
|
||||
return style
|
||||
|
||||
raw_medium = style.get("medium")
|
||||
medium = canon_medium(raw_medium) if raw_medium is not None else None
|
||||
has_photo = bool(style.get("photo"))
|
||||
has_art = bool(style.get("art_style"))
|
||||
|
||||
# Decide the branch. A recognized medium is authoritative; otherwise infer from
|
||||
# whichever render key the (old) data already had, defaulting to photo.
|
||||
if medium in MEDIUM_OPTIONS:
|
||||
photo_branch = medium == "photograph"
|
||||
elif has_art and not has_photo:
|
||||
photo_branch = False
|
||||
else:
|
||||
photo_branch = True
|
||||
|
||||
photo_val = style.get("photo") if has_photo else None
|
||||
art_val = style.get("art_style") if has_art else None
|
||||
|
||||
out = OrderedDict()
|
||||
if "aesthetics" in style:
|
||||
out["aesthetics"] = style["aesthetics"]
|
||||
if "lighting" in style:
|
||||
out["lighting"] = style["lighting"]
|
||||
|
||||
if photo_branch:
|
||||
# aesthetics, lighting, photo, medium, color_palette
|
||||
val = photo_val if photo_val is not None else art_val
|
||||
if val is not None:
|
||||
out["photo"] = val
|
||||
if medium is not None:
|
||||
out["medium"] = medium
|
||||
else:
|
||||
# aesthetics, lighting, medium, art_style, color_palette
|
||||
if medium is not None:
|
||||
out["medium"] = medium
|
||||
val = art_val if art_val is not None else photo_val
|
||||
if val is not None:
|
||||
out["art_style"] = val
|
||||
|
||||
pal = sanitize_palette(style.get("color_palette"), MAX_IMAGE_PALETTE)
|
||||
if pal is not None:
|
||||
out["color_palette"] = pal
|
||||
|
||||
# Preserve any unexpected extra keys at the end rather than dropping them.
|
||||
for k, v in style.items():
|
||||
if k not in (
|
||||
"aesthetics",
|
||||
"lighting",
|
||||
"photo",
|
||||
"art_style",
|
||||
"medium",
|
||||
"color_palette",
|
||||
):
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def normalize_element(el):
|
||||
"""Reorder an element's keys to the strict schema order and uppercase its
|
||||
palette. obj: type, bbox, desc, color_palette. text: type, bbox, text, desc,
|
||||
color_palette. bbox is kept verbatim (already [y1,x1,y2,x2] in stored form)."""
|
||||
if not isinstance(el, dict):
|
||||
return el
|
||||
etype = el.get("type", "obj")
|
||||
out = OrderedDict()
|
||||
out["type"] = etype
|
||||
if el.get("bbox") is not None:
|
||||
out["bbox"] = el["bbox"]
|
||||
if etype == "text":
|
||||
if "text" in el:
|
||||
out["text"] = el["text"]
|
||||
if "desc" in el:
|
||||
out["desc"] = el["desc"]
|
||||
else:
|
||||
if "desc" in el:
|
||||
out["desc"] = el["desc"]
|
||||
pal = sanitize_palette(el.get("color_palette"), MAX_ELEMENT_PALETTE)
|
||||
if pal is not None:
|
||||
out["color_palette"] = pal
|
||||
# Preserve any extras (e.g. future keys) at the end.
|
||||
for k, v in el.items():
|
||||
if k not in out and k != "color_palette":
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def normalize_caption_dict(data):
|
||||
"""Normalize a parsed caption dict in place-ish: drop input-only aspect_ratio,
|
||||
enforce top-level key order, normalize style (photo/art_style branch) and every
|
||||
element. Returns a new OrderedDict. Accepts old-format captions and emits new."""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
data.pop("aspect_ratio", None) # input-only context, never part of output
|
||||
|
||||
out = OrderedDict()
|
||||
if "high_level_description" in data:
|
||||
out["high_level_description"] = data["high_level_description"]
|
||||
if "style_description" in data:
|
||||
out["style_description"] = normalize_style(data["style_description"])
|
||||
|
||||
decon = data.get("compositional_deconstruction")
|
||||
if isinstance(decon, dict):
|
||||
nd = OrderedDict()
|
||||
if "background" in decon:
|
||||
nd["background"] = decon["background"]
|
||||
els = decon.get("elements")
|
||||
if isinstance(els, list):
|
||||
nd["elements"] = [normalize_element(e) for e in els]
|
||||
for k, v in decon.items():
|
||||
if k not in ("background", "elements"):
|
||||
nd[k] = v
|
||||
out["compositional_deconstruction"] = nd
|
||||
elif decon is not None:
|
||||
out["compositional_deconstruction"] = decon
|
||||
|
||||
for k, v in data.items():
|
||||
if k not in (
|
||||
"high_level_description",
|
||||
"style_description",
|
||||
"compositional_deconstruction",
|
||||
"aspect_ratio",
|
||||
):
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def is_ideogram_caption_str(text):
|
||||
"""True if text parses as a JSON object with a compositional_deconstruction block."""
|
||||
t = (text or "").strip()
|
||||
if not t.startswith("{"):
|
||||
return False
|
||||
try:
|
||||
d = json.loads(t)
|
||||
except Exception:
|
||||
return False
|
||||
return isinstance(d, dict) and isinstance(
|
||||
d.get("compositional_deconstruction"), dict
|
||||
)
|
||||
|
||||
|
||||
def to_model_string(data):
|
||||
"""Serialize a caption dict to the compact, model-ready string the renderer wants."""
|
||||
return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest_caption_string(text):
|
||||
"""Parse, normalize (migrating old format), and return the compact model-ready
|
||||
string. Returns the input unchanged if it is not an Ideogram structured caption
|
||||
(plain-text captions pass straight through)."""
|
||||
t = (text or "").strip()
|
||||
if not t.startswith("{"):
|
||||
return text
|
||||
try:
|
||||
data = json.loads(t, object_pairs_hook=OrderedDict)
|
||||
except Exception:
|
||||
return text
|
||||
if not (
|
||||
isinstance(data, dict)
|
||||
and isinstance(data.get("compositional_deconstruction"), dict)
|
||||
):
|
||||
return text
|
||||
return to_model_string(normalize_caption_dict(data))
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
if REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
from toolkit.ideogram_caption import normalize_caption_dict
|
||||
|
||||
# The generation prompt lives here. It's a `name = """<content>"""` file, but the
|
||||
# content intentionally contains literal `\uNNNN` and `\n` sequences that are not
|
||||
# valid Python escapes, so it cannot be imported -- we read the triple-quoted
|
||||
|
|
@ -127,62 +129,22 @@ def sanitize_bbox(bbox):
|
|||
return [y1, x1, y2, x2]
|
||||
|
||||
|
||||
HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
||||
|
||||
|
||||
def sanitize_palette(palette, max_len):
|
||||
"""Keep unique, valid hex colors in order, capped to max_len. Returns the
|
||||
cleaned list, or None if nothing valid remains (drop the key)."""
|
||||
if not isinstance(palette, (list, tuple)):
|
||||
return None
|
||||
seen = set()
|
||||
out = []
|
||||
for c in palette:
|
||||
if not isinstance(c, str):
|
||||
continue
|
||||
c = c.strip()
|
||||
if not HEX_COLOR_RE.match(c):
|
||||
continue
|
||||
key = c.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(c)
|
||||
if len(out) >= max_len:
|
||||
break
|
||||
return out or None
|
||||
|
||||
|
||||
def sanitize_caption(data: dict) -> dict:
|
||||
"""Light cleanup: drop any aspect_ratio key (input-only context, not output),
|
||||
clean each bbox, and cap color palettes (16 per image, 5 per element)."""
|
||||
data.pop("aspect_ratio", None)
|
||||
style = data.get("style_description")
|
||||
if isinstance(style, dict) and "color_palette" in style:
|
||||
pal = sanitize_palette(style["color_palette"], 16)
|
||||
if pal is None:
|
||||
style.pop("color_palette", None)
|
||||
else:
|
||||
style["color_palette"] = pal
|
||||
"""Clamp each bbox to valid 0-1000 [y1,x1,y2,x2], then hand off to the shared
|
||||
normalizer for the rest: drop aspect_ratio, enforce the photo/art_style branch
|
||||
and key order, canonicalize medium, and cap/uppercase color palettes (16 per
|
||||
image, 5 per element)."""
|
||||
decon = data.get("compositional_deconstruction", {})
|
||||
elements = decon.get("elements", [])
|
||||
elements = decon.get("elements", []) if isinstance(decon, dict) else []
|
||||
if isinstance(elements, list):
|
||||
for el in elements:
|
||||
if not isinstance(el, dict):
|
||||
continue
|
||||
if "bbox" in el:
|
||||
if isinstance(el, dict) and "bbox" in el:
|
||||
cleaned = sanitize_bbox(el["bbox"])
|
||||
if cleaned is None:
|
||||
el.pop("bbox", None)
|
||||
else:
|
||||
el["bbox"] = cleaned
|
||||
if "color_palette" in el:
|
||||
pal = sanitize_palette(el["color_palette"], 5)
|
||||
if pal is None:
|
||||
el.pop("color_palette", None)
|
||||
else:
|
||||
el["color_palette"] = pal
|
||||
return data
|
||||
return normalize_caption_dict(data)
|
||||
|
||||
|
||||
def upsample_one(
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
VERSION = "0.10.11"
|
||||
VERSION = "0.10.12"
|
||||
|
|
|
|||
Loading…
Reference in New Issue