Merge pull request #8 from by-ae/cursor/region-store-caption-fixes-0d7f

Fix region placement, caption crashes, and store recovery
This commit is contained in:
cursor[bot]
2026-10-27 21:03:40 +00:00
committed by GitHub
11 changed files with 409 additions and 66 deletions
+2 -2
View File
@@ -68,5 +68,5 @@ jobs:
- name: Lint JavaScript
run: npm run lint
- name: Test caption layout helpers
run: node --test tests/caption_layout.test.js
- name: Test JavaScript helpers
run: node --test tests/*.test.js
+5 -3
View File
@@ -58,11 +58,13 @@ The builder outputs a structured JSON prompt compatible with Ideogram 4's API fo
This is usually harmless. The store routes register on startup, and if they fail, character/object/scene save features may not work, but the builder itself will function normally. Check file permissions on your ComfyUI user directory.
Load shows the request error (for example `HTTP 500`) when the store cannot be read. It says there are no saved scenes or characters only when the server returns an empty library. A failed request does not mean the presets were deleted.
**Store location:** Character, object, and scene presets are stored in `<ComfyUI user directory>/byae-ideogram-tools/` as JSON files (`characters.json`, `objects.json`, `scenes.json`).
**Unicode support:** Preset names can include Unicode characters (e.g. 角色, Персонаж, Émilie). Names must start with a letter, number, or underscore, and can contain letters, numbers, spaces, underscores, and hyphens (up to 64 characters).
**Corrupt file recovery:** If a store file becomes corrupted (invalid JSON), it will be automatically moved to a backup file named `<bucket>.json.corrupt-<timestamp>` and the bucket will be treated as empty. Other buckets continue to work normally, and saves to the recovered bucket will work immediately.
**Corrupt file recovery:** If a store file cannot be used as a bucket (invalid JSON, or JSON that is not an object), it is moved to a backup named `<bucket>.json.corrupt-<timestamp>` and the bucket is treated as empty. When that backup name already exists, the new file gets an extra suffix so the earlier backup is kept. Other buckets continue to work normally, and saves to the recovered bucket work immediately.
### Failed to save character/object/scene
@@ -97,8 +99,8 @@ pip install pytest pytest-asyncio aiohttp
# Run all tests
python -m pytest tests/ -v
# Region layout helpers (no Python dependencies)
node --test tests/caption_layout.test.js
# Region layout and store-client helpers (no Python dependencies)
node --test tests/*.test.js
# Run specific test file
python -m pytest tests/test_store.py -v
+86 -28
View File
@@ -230,14 +230,10 @@ def _render_preview(boxes, width, height, bg_guid="", background_opacity=0.5):
if not isinstance(box, dict) or box.get("nobbox"):
continue
r, g, b = _hex_rgb(PREVIEW_PALETTE[i % len(PREVIEW_PALETTE)])
x1 = max(0, min(rw, round(box.get("x", 0) * rw)))
y1 = max(0, min(rh, round(box.get("y", 0) * rh)))
x2 = max(0, min(rw, round((box.get("x", 0) + box.get("w", 0)) * rw)))
y2 = max(0, min(rh, round((box.get("y", 0) + box.get("h", 0)) * rh)))
if x2 < x1:
x1, x2 = x2, x1
if y2 < y1:
y1, y2 = y2, y1
edges = _preview_edges(box, rw, rh)
if edges is None:
continue
x1, y1, x2, y2 = edges
draw.rectangle([x1, y1, x2, y2], outline=(r, g, b, 255), width=2)
@@ -271,12 +267,45 @@ def _render_preview(boxes, width, height, bg_guid="", background_opacity=0.5):
return torch.from_numpy(arr).unsqueeze(0)
def _norm_bbox(box):
def c(v):
return max(0, min(1000, round(v * 1000)))
def _scale_axis(value):
"""Map one finite axis onto Ideogram's integer 0-1000 range.
x, y, w, h = box.get("x", 0.0), box.get("y", 0.0), box.get("w", 0.0), box.get("h", 0.0)
ymin, xmin, ymax, xmax = c(y), c(x), c(y + h), c(x + w)
Clamp before rounding. round() raises OverflowError once value * 1000
exceeds the float range, which a hand-edited region can do.
"""
if value >= 1:
return 1000
if value <= 0:
return 0
return int(round(value * 1000))
def _box_coords(box):
"""Return x, y, w, h as finite floats, or None when any value is unusable."""
if not isinstance(box, dict):
return None
coords = [_finite_coord(box.get(key, 0.0)) for key in ("x", "y", "w", "h")]
if any(value is None for value in coords):
return None
return coords
def _norm_bbox(box):
"""Return [ymin, xmin, ymax, xmax], or None when a coordinate is unusable.
Numeric strings are accepted. Booleans are not: True is an int, and
treating it as 1 used to pin an edge to the far side of the canvas.
"""
coords = _box_coords(box)
if coords is None:
return None
x, y, w, h = coords
ymin, xmin, ymax, xmax = (
_scale_axis(y),
_scale_axis(x),
_scale_axis(y + h),
_scale_axis(x + w),
)
if ymin > ymax:
ymin, ymax = ymax, ymin
if xmin > xmax:
@@ -284,6 +313,28 @@ def _norm_bbox(box):
return [ymin, xmin, ymax, xmax]
def _preview_edges(box, rw, rh):
"""Pixel edges for one region, or None when its coordinates are unusable."""
coords = _box_coords(box)
if coords is None:
return None
x, y, w, h = coords
def edge(value, scale):
if value >= 1:
return scale
if value <= 0:
return 0
return max(0, min(scale, int(round(value * scale))))
x1, y1, x2, y2 = edge(x, rw), edge(y, rh), edge(x + w, rw), edge(y + h, rh)
if x2 < x1:
x1, x2 = x2, x1
if y2 < y1:
y1, y2 = y2, y1
return x1, y1, x2, y2
# Ideogram 4 captions accept uppercase #RRGGBB only: 16 style colors, 5 per element.
# https://github.com/ideogram-oss/ideogram4/blob/main/docs/prompting.md
_HEX6 = re.compile(r"^#[0-9A-F]{6}$")
@@ -338,6 +389,27 @@ def _dumps(v, lvl=0):
return json.dumps(v, ensure_ascii=False)
def _elements_from_boxes(boxes):
elements = []
for box in boxes or []:
if not isinstance(box, dict):
continue
etype = "text" if box.get("type") == "text" else "obj"
elem = {"type": etype}
if not box.get("nobbox"):
bbox = _norm_bbox(box)
if bbox is not None:
elem["bbox"] = bbox
if etype == "text":
elem["text"] = box.get("text", "")
elem["desc"] = box.get("desc", "")
palette = _palette(box.get("palette", []), ELEMENT_PALETTE_MAX)
if palette:
elem["color_palette"] = palette
elements.append(elem)
return elements
def _parse_json_list(s):
if s:
try:
@@ -537,21 +609,7 @@ folder and picked up automatically by the editor.""",
sd["color_palette"] = palette
caption["style_description"] = sd
elements = []
for box in boxes:
if not isinstance(box, dict):
continue
etype = "text" if box.get("type") == "text" else "obj"
elem = {"type": etype}
if not box.get("nobbox"):
elem["bbox"] = _norm_bbox(box)
if etype == "text":
elem["text"] = box.get("text", "")
elem["desc"] = box.get("desc", "")
palette = _palette(box.get("palette", []), ELEMENT_PALETTE_MAX)
if palette:
elem["color_palette"] = palette
elements.append(elem)
elements = _elements_from_boxes(boxes)
caption["compositional_deconstruction"] = {
"background": background,
+23 -12
View File
@@ -60,26 +60,37 @@ def _validate_name(name: str) -> str:
return name
def _backup_unreadable(path: str, reason) -> None:
"""Move a store file aside. A name that already exists is not replaced."""
timestamp = int(time.time())
backup_path = f"{path}.corrupt-{timestamp}"
if os.path.exists(backup_path):
backup_path = f"{path}.corrupt-{timestamp}-{time.time_ns()}"
logger.warning(
"ByAE Ideogram: corrupt store file %s (%s), moving to %s",
path, reason, backup_path,
)
try:
os.rename(path, backup_path)
except OSError as rename_err:
logger.error("ByAE Ideogram: failed to backup corrupt file: %s", rename_err)
def _read_json(path: str) -> dict:
if not os.path.isfile(path):
return {}
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except (json.JSONDecodeError, UnicodeDecodeError) as e:
# Move corrupt file aside and treat bucket as empty
timestamp = int(time.time())
backup_path = f"{path}.corrupt-{timestamp}"
logger.warning(
"ByAE Ideogram: corrupt store file %s (%s), moving to %s",
path, e, backup_path
)
try:
os.rename(path, backup_path)
except OSError as rename_err:
logger.error("ByAE Ideogram: failed to backup corrupt file: %s", rename_err)
_backup_unreadable(path, e)
return {}
if isinstance(data, dict):
return data
# A JSON array or other value is valid JSON but not a bucket. Leaving it
# in place made the next save overwrite it with no backup.
_backup_unreadable(path, f"expected a JSON object, got {type(data).__name__}")
return {}
def _write_json(path: str, data: dict) -> None:
+75
View File
@@ -7,7 +7,10 @@ import {
fallbackRegion,
ideogramBboxToRect,
offsetAddedRegions,
pasteRegions,
rectToIdeogramBbox,
regionCoordsFinite,
staggerLoadedRegion,
} from "../web/js/caption_layout.js";
describe("ideogram bbox import", () => {
@@ -72,6 +75,17 @@ describe("ideogram bbox import", () => {
assert.equal(regionCoordsFinite({ x: NaN, y: 0.2, w: 0.3, h: 0.4 }), false);
assert.equal(regionCoordsFinite({ x: 0.1, y: 0.2, w: 0.3 }), false);
});
it("exports a usable bbox and drops coordinates that are not finite", () => {
assert.deepEqual(rectToIdeogramBbox({ x: 0.1, y: 0.2, w: 0.3, h: 0.4 }), [200, 100, 600, 400]);
assert.deepEqual(rectToIdeogramBbox({ x: "0.1", y: "0.2", w: "0.3", h: "0.4" }), [200, 100, 600, 400]);
assert.deepEqual(rectToIdeogramBbox({ x: 0.5, y: 0.6, w: -0.2, h: -0.1 }), [500, 300, 600, 500]);
assert.equal(rectToIdeogramBbox({ x: Number.NaN, y: 0.2, w: 0.3, h: 0.4 }), null);
assert.equal(rectToIdeogramBbox({ x: "nope", y: 0, w: 0.2, h: 0.2 }), null);
assert.equal(rectToIdeogramBbox({ x: true, y: 0, w: 0.2, h: 0.2 }), null);
assert.deepEqual(rectToIdeogramBbox({}), [0, 0, 0, 0]);
assert.deepEqual(rectToIdeogramBbox({ x: 1e308, y: 0, w: 1e308, h: 0.2 }), [0, 1000, 200, 1000]);
});
});
describe("color palettes", () => {
@@ -121,3 +135,64 @@ describe("added scenes", () => {
near(placed[0].w, 0.05);
});
});
describe("pasted regions", () => {
it("nudges by the paste offset and stays inside the canvas", () => {
const placed = pasteRegions([
{ x: 0.9, y: 0.9, w: 0.2, h: 0.2, desc: "edge", palette: ["#FFFFFF"] },
]);
near(placed[0].x, 0.93);
near(placed[0].y, 0.93);
near(placed[0].w, 0.07);
near(placed[0].h, 0.07);
assert.ok(placed[0].x + placed[0].w <= 1 + 1e-9);
assert.ok(placed[0].y + placed[0].h <= 1 + 1e-9);
assert.equal(placed[0].desc, "edge");
});
it("does not keep the source store link or share its palette array", () => {
const src = [{
x: 0.1, y: 0.2, w: 0.2, h: 0.2,
palette: ["#FFFFFF"],
storeRef: { type: "character", name: "Ada" },
}];
const placed = pasteRegions(src);
near(placed[0].x, 0.13);
near(placed[0].y, 0.23);
near(placed[0].w, 0.2);
assert.equal(placed[0].storeRef, undefined);
placed[0].palette.push("#000000");
assert.deepEqual(src[0].palette, ["#FFFFFF"]);
assert.deepEqual(src[0].storeRef, { type: "character", name: "Ada" });
});
});
describe("loaded characters and objects", () => {
it("keeps the original diagonal stagger while the region still fits", () => {
for (let i = 0; i <= 16; i++) {
const box = staggerLoadedRegion(i);
near(box.x, 0.1 + i * 0.04);
near(box.y, 0.1 + i * 0.04);
near(box.w, 0.25);
near(box.h, 0.2);
assert.ok(box.x + box.w <= 1 + 1e-9, `index ${i} leaves the canvas`);
}
});
it("tiles later regions inside the canvas instead of sliding off it", () => {
const box = staggerLoadedRegion(17);
near(box.x, 0);
near(box.y, 0);
near(box.w, 0.25);
near(box.h, 0.2);
const next = staggerLoadedRegion(18);
near(next.x, 0.25);
near(next.y, 0);
for (let i = 0; i < 80; i++) {
const placed = staggerLoadedRegion(i);
assert.ok(placed.x >= -1e-9 && placed.y >= -1e-9, `index ${i} origin`);
assert.ok(placed.x + placed.w <= 1 + 1e-9, `index ${i} x`);
assert.ok(placed.y + placed.h <= 1 + 1e-9, `index ${i} y`);
}
});
});
+55
View File
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { afterEach, describe, it } from "node:test";
globalThis.window = {
comfyAPI: {
app: {},
api: { addEventListener() {} },
},
};
const { loadStores } = await import("../web/js/ideogram4_shared.js");
describe("loadStores", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("returns the three buckets from a successful response", async () => {
globalThis.fetch = async () => ({
ok: true,
json: async () => ({
characters: { Ada: { desc: "a" } },
objects: {},
scenes: { Scene: { boxes: [] } },
}),
});
const stores = await loadStores();
assert.deepEqual(stores.characters.Ada, { desc: "a" });
assert.deepEqual(stores.objects, {});
assert.deepEqual(stores.scenes.Scene, { boxes: [] });
});
it("returns empty buckets when the server has no presets", async () => {
globalThis.fetch = async () => ({
ok: true,
json: async () => ({}),
});
const stores = await loadStores();
assert.deepEqual(stores, { characters: {}, objects: {}, scenes: {} });
});
it("throws when the store request fails instead of looking empty", async () => {
globalThis.fetch = async () => ({ ok: false, status: 500, json: async () => ({}) });
await assert.rejects(loadStores(), /HTTP 500/);
});
it("throws when the network request fails", async () => {
globalThis.fetch = async () => {
throw new Error("network down");
};
await assert.rejects(loadStores(), /network down/);
});
});
+40
View File
@@ -7,6 +7,7 @@ import unittest
from nodes.ideogram4_nodes import (
sanitize_bg_guid, _norm_bbox, _palette, _dumps, _parse_json_list,
_caption_to_boxes, _coerce_layout_int, _coerce_opacity,
_elements_from_boxes, _preview_edges,
save_bg_image_to_temp,
)
@@ -58,6 +59,45 @@ class TestPromptHelpers(unittest.TestCase):
result = _norm_bbox(box)
self.assertEqual(result, [0, 0, 0, 0])
def test_norm_bbox_accepts_numeric_strings(self):
box = {"x": "0.1", "y": "0.2", "w": "0.3", "h": "0.4"}
self.assertEqual(_norm_bbox(box), [200, 100, 600, 400])
def test_norm_bbox_rejects_non_finite_coordinates(self):
self.assertIsNone(_norm_bbox({"x": float("nan"), "y": 0.2, "w": 0.3, "h": 0.4}))
self.assertIsNone(_norm_bbox({"x": "nope", "y": 0.2, "w": 0.3, "h": 0.4}))
self.assertIsNone(_norm_bbox({"x": True, "y": 0.0, "w": 0.2, "h": 0.2}))
self.assertIsNone(_norm_bbox({"x": float("inf"), "y": 0.0, "w": 0.2, "h": 0.2}))
def test_norm_bbox_huge_finite_values_do_not_raise(self):
result = _norm_bbox({"x": 1e308, "y": 0.0, "w": 1e308, "h": 0.2})
self.assertEqual(result, [0, 1000, 200, 1000])
def test_elements_from_boxes_omits_unusable_bbox(self):
elements = _elements_from_boxes([
{"type": "obj", "desc": "bad", "x": "nope", "y": 0, "w": 0.2, "h": 0.2},
{
"type": "text", "text": "Hi", "desc": "ok",
"x": 0.1, "y": 0.2, "w": 0.3, "h": 0.4,
},
"skip",
{"type": "obj", "desc": "no box", "nobbox": True, "x": 0.1, "y": 0.1, "w": 0.2, "h": 0.2},
])
self.assertEqual(len(elements), 3)
self.assertNotIn("bbox", elements[0])
self.assertEqual(elements[0]["desc"], "bad")
self.assertEqual(elements[1]["bbox"], [200, 100, 600, 400])
self.assertEqual(elements[1]["text"], "Hi")
self.assertEqual(list(elements[1].keys()), ["type", "bbox", "text", "desc"])
self.assertNotIn("bbox", elements[2])
def test_preview_edges_skip_unusable_box(self):
self.assertIsNone(_preview_edges({"x": "nope", "y": 0, "w": 0.2, "h": 0.2}, 100, 50))
self.assertEqual(
_preview_edges({"x": 0.1, "y": 0.2, "w": 0.3, "h": 0.4}, 100, 50),
(10, 10, 40, 30),
)
def test_palette_filters_empty_strings(self):
colors = {"c1": "#FF0000", "c2": "", "c3": "#00FF00", "c4": None}
result = _palette(colors)
+48
View File
@@ -144,6 +144,54 @@ class TestStore(unittest.TestCase):
self.assertIn("ValidObject", stores["objects"])
self.assertEqual(stores["objects"]["ValidObject"]["data"], "test")
def test_non_object_json_is_backed_up_before_overwrite(self):
bucket_path = os.path.join(self.temp_dir, "byae-ideogram-tools", "characters.json")
os.makedirs(os.path.dirname(bucket_path), exist_ok=True)
original = '[{"desc": "keep me"}]'
with open(bucket_path, "w", encoding="utf-8") as f:
f.write(original)
self.assertEqual(load_bucket("characters"), {})
self.assertFalse(os.path.exists(bucket_path))
backup_files = [
name for name in os.listdir(os.path.dirname(bucket_path))
if name.startswith("characters.json.corrupt-")
]
self.assertEqual(len(backup_files), 1)
with open(os.path.join(os.path.dirname(bucket_path), backup_files[0]), encoding="utf-8") as f:
self.assertEqual(f.read(), original)
save_entry("characters", "New", {"desc": "n"})
bucket = load_bucket("characters")
self.assertEqual(bucket["New"]["desc"], "n")
with open(os.path.join(os.path.dirname(bucket_path), backup_files[0]), encoding="utf-8") as f:
self.assertEqual(f.read(), original)
def test_second_corrupt_backup_in_the_same_second_keeps_the_first(self):
store_dir = os.path.join(self.temp_dir, "byae-ideogram-tools")
os.makedirs(store_dir, exist_ok=True)
bucket_path = os.path.join(store_dir, "objects.json")
stamp = 1_700_000_000
existing = os.path.join(store_dir, f"objects.json.corrupt-{stamp}")
with open(existing, "w", encoding="utf-8") as f:
f.write("first")
with open(bucket_path, "w", encoding="utf-8") as f:
f.write("{not json")
with patch("store.time.time", return_value=stamp):
self.assertEqual(load_bucket("objects"), {})
self.assertFalse(os.path.exists(bucket_path))
with open(existing, encoding="utf-8") as f:
self.assertEqual(f.read(), "first")
extras = [
name for name in os.listdir(store_dir)
if name.startswith(f"objects.json.corrupt-{stamp}-")
]
self.assertEqual(len(extras), 1)
with open(os.path.join(store_dir, extras[0]), encoding="utf-8") as f:
self.assertEqual(f.read(), "{not json")
def test_save_works_after_corrupt_file_recovery(self):
# Create corrupt file
bucket_path = os.path.join(self.temp_dir, "byae-ideogram-tools", "scenes.json")
+64
View File
@@ -35,6 +35,29 @@ export function ideogramBboxToRect(bbox) {
};
}
function scaleAxis(value) {
if (value >= 1) return 1000;
if (value <= 0) return 0;
return Math.max(0, Math.min(1000, Math.round(value * 1000)));
}
/** Editor x/y/w/h (0-1) to Ideogram [ymin, xmin, ymax, xmax], or null if unusable. */
export function rectToIdeogramBbox(box) {
if (!box || typeof box !== "object") return null;
const x = finiteCoord(box.x ?? 0);
const y = finiteCoord(box.y ?? 0);
const w = finiteCoord(box.w ?? 0);
const h = finiteCoord(box.h ?? 0);
if (x == null || y == null || w == null || h == null) return null;
let ymin = scaleAxis(y);
let xmin = scaleAxis(x);
let ymax = scaleAxis(y + h);
let xmax = scaleAxis(x + w);
if (ymin > ymax) [ymin, ymax] = [ymax, ymin];
if (xmin > xmax) [xmin, xmax] = [xmax, xmin];
return [ymin, xmin, ymax, xmax];
}
export function fallbackRegion(index) {
const i = Number.isInteger(index) && index >= 0 ? index : 0;
const row = Math.floor(i / 4);
@@ -119,3 +142,44 @@ export function offsetAddedRegions(boxes, nudge = 0.05) {
y: (typeof b.y === "number" && Number.isFinite(b.y) ? b.y : 0) + nudge,
}));
}
/** Ctrl+V: nudge a copy so it is visible, and do not keep the source store link. */
export function pasteRegions(boxes, nudge = 0.03) {
return offsetAddedRegions(boxes, nudge).map((b) => {
const copy = {
...b,
palette: Array.isArray(b.palette) ? b.palette.slice() : [],
};
delete copy.storeRef;
return copy;
});
}
/**
* Place a character/object loaded into the scene.
* Indexes 0-16 keep the original 0.04 diagonal from (0.1, 0.1). Later indexes
* tile a 0.25x0.20 grid that stays inside the canvas.
*/
export function staggerLoadedRegion(index) {
const origin = 0.1;
const step = 0.04;
const w = 0.25;
const h = 0.2;
const i = Number.isInteger(index) && index >= 0 ? index : 0;
const diagonalCount = Math.floor((1 - w - origin) / step) + 1;
if (i < diagonalCount) {
return normalizeBoxCoords({
x: origin + i * step,
y: origin + i * step,
w,
h,
});
}
const extra = i - diagonalCount;
const cols = Math.max(1, Math.floor((1 - w) / w) + 1);
const rows = Math.max(1, Math.floor((1 - h) / h) + 1);
const slot = extra % (cols * rows);
const col = slot % cols;
const row = Math.floor(slot / cols);
return normalizeBoxCoords({ x: col * w, y: row * h, w, h });
}
+9 -18
View File
@@ -7,7 +7,8 @@ import {
loadStores, saveStoreEntry, installBuilderQueueHooks, installBgUpdateListener,
} from "./ideogram4_shared.js";
import {
cleanPalette, elementToRegion, offsetAddedRegions, regionCoordsFinite,
cleanPalette, elementToRegion, offsetAddedRegions, pasteRegions, rectToIdeogramBbox,
regionCoordsFinite, staggerLoadedRegion,
} from "./caption_layout.js";
const { app } = window.comfyAPI.app;
@@ -1527,11 +1528,7 @@ app.registerExtension({
e.stopPropagation();
if (!node._clipBoxes?.length) return;
const newIdxs = [];
for (const cb of node._clipBoxes) {
const nb = cloneBox(cb);
nb.x = clamp01(nb.x + 0.03);
nb.y = clamp01(nb.y + 0.03);
delete nb.storeRef;
for (const nb of pasteRegions(node._clipBoxes.map((b) => cloneBox(b)))) {
node._boxes.push(nb);
newIdxs.push(node._boxes.length - 1);
}
@@ -1660,14 +1657,6 @@ app.registerExtension({
return widgetString(findW(name)?.value);
}
function normBboxJS(b) {
const c = (v) => Math.max(0, Math.min(1000, Math.round(v * 1000)));
let ymin = c(b.y), xmin = c(b.x), ymax = c(b.y + b.h), xmax = c(b.x + b.w);
if (ymin > ymax) [ymin, ymax] = [ymax, ymin];
if (xmin > xmax) [xmin, xmax] = [xmax, xmin];
return [ymin, xmin, ymax, xmax];
}
function buildCaption() {
const cap = {};
const hld = getW("high_level_description").trim();
@@ -1684,7 +1673,10 @@ app.registerExtension({
const elements = node._boxes.map((b) => {
const etype = b.type === "text" ? "text" : "obj";
const el = { type: etype };
if (!b.nobbox) el.bbox = normBboxJS(b);
if (!b.nobbox) {
const bbox = rectToIdeogramBbox(b);
if (bbox) el.bbox = bbox;
}
if (etype === "text") el.text = b.text || "";
el.desc = b.desc || "";
const pal = cleanPalette(b.palette, MAX_ELEM_COLORS);
@@ -2011,10 +2003,9 @@ app.registerExtension({
const bucket = type === "object" ? stores.objects : stores.characters;
const data = bucket[name];
if (!data) return;
const offset = node._boxes.length * 0.04;
const placed = staggerLoadedRegion(node._boxes.length);
const nb = {
x: clamp01(0.1 + offset), y: clamp01(0.1 + offset),
w: 0.25, h: 0.2,
x: placed.x, y: placed.y, w: placed.w, h: placed.h,
type: data.type || "obj",
text: data.text || "",
desc: data.desc || "",
+2 -3
View File
@@ -8,8 +8,6 @@ export const BUILDER_CLASS = "Ideogram4PromptBuilderKJEnhancedByAE";
export const PREVIEW_UPDATE_CLASS = "Ideogram4PromptBuilderKJPreviewUpdateByAE";
const STORE_API = "/byae/ideogram/stores";
const EMPTY_STORES = () => ({ characters: {}, objects: {}, scenes: {} });
export async function loadStores() {
try {
const resp = await fetch(STORE_API, {
@@ -23,8 +21,9 @@ export async function loadStores() {
scenes: data.scenes || {},
};
} catch (e) {
// Callers distinguish a failed request from a library that is actually empty.
console.warn("ByAE Ideogram: failed to load stores", e);
return EMPTY_STORES();
throw e;
}
}