@hiper2d/ai-agents 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/images.d.mts +17 -8
- package/dist/images.d.ts +17 -8
- package/dist/images.js +32 -4
- package/dist/images.js.map +1 -1
- package/dist/images.mjs +32 -4
- package/dist/images.mjs.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +27 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,9 +100,10 @@ const { portraits, sheet, costUSD } = await drawPortraitSheet(googleKey, sharp,
|
|
|
100
100
|
The pure geometry (`fitFraming`, `circleFocus`, `focusToBackground`, …) is safe in a
|
|
101
101
|
browser bundle for reframe editors and avatar renderers.
|
|
102
102
|
|
|
103
|
-
A blocked drawing (Gemini answers HTTP
|
|
104
|
-
|
|
105
|
-
and
|
|
103
|
+
A blocked drawing (Gemini answers HTTP 400 with an "Image generation blocked due to safety
|
|
104
|
+
violations" / "prohibited content guidelines" body — the usual shape — or HTTP 200 with no
|
|
105
|
+
image and an `errors[]` code such as `image_safety`) throws `ImageRefusalError` with the
|
|
106
|
+
`code` and Gemini's `reason`; it is not retryable with the same prompt. Check it with
|
|
106
107
|
`isImageRefusalError(error)` — by name, since this subpath is bundled apart from the main
|
|
107
108
|
entry's `ModelRefusalError`. Any other imageless reply is a plain `Error` naming the
|
|
108
109
|
interaction `status` and error code.
|
package/dist/images.d.mts
CHANGED
|
@@ -216,19 +216,27 @@ interface GenerateImageOptions {
|
|
|
216
216
|
fetchImpl?: typeof fetch;
|
|
217
217
|
}
|
|
218
218
|
/**
|
|
219
|
-
* Gemini's Interactions API
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
219
|
+
* Gemini's Interactions API blocks a drawing in two shapes (both seen in production,
|
|
220
|
+
* 2026-10-13):
|
|
221
|
+
*
|
|
222
|
+
* - **HTTP 400** with `{"error": {"message": "Image generation blocked due to safety
|
|
223
|
+
* violations. Please modify your input and retry.", "code": "Unable to show the generated
|
|
224
|
+
* image. The image was filtered out because it violated Google's Generative AI Prohibited
|
|
225
|
+
* Use policy …"}}` — or "Request blocked due to prohibited content guidelines …". This is
|
|
226
|
+
* what an explicit prompt gets.
|
|
227
|
+
* - **HTTP 200** with no image block, `status: "failed" | "incomplete"` and an `errors[]`
|
|
228
|
+
* entry whose `code` names the block (https://ai.google.dev/gemini-api/docs/api-errors,
|
|
229
|
+
* "Generation blocked"): `image_safety`, `image_prohibited_content`, `image_recitation`,
|
|
230
|
+
* `image_other`, plus the text-side `safety`, `prohibited_content`, `recitation`,
|
|
231
|
+
* `blocklist`, `spii`, `content_blocked`. `no_image` is a plain generation failure.
|
|
225
232
|
*
|
|
226
233
|
* Deliberately NOT a `ModelRefusalError` subclass: this subpath is bundled apart from the
|
|
227
234
|
* main entry, so an `instanceof` against the main entry's class would never match. Hosts
|
|
228
235
|
* check `isImageRefusalError(error)` (by name) instead. Not retryable on the same prompt.
|
|
229
236
|
*/
|
|
230
237
|
declare class ImageRefusalError extends Error {
|
|
231
|
-
/** The block code as Gemini sent it (a URI or bare `snake_case`
|
|
238
|
+
/** The block code as Gemini sent it: an `errors[].code` (a URI or bare `snake_case`
|
|
239
|
+
* name) on the HTTP 200 shape, or `http_400` on the error-body shape. */
|
|
232
240
|
code: string;
|
|
233
241
|
/** Gemini's human-readable message, when it sent one. */
|
|
234
242
|
reason?: string;
|
|
@@ -243,7 +251,8 @@ declare function isImageRefusalError(error: unknown): error is ImageRefusalError
|
|
|
243
251
|
* IMAGE, not per pixel (a 1K and a 2K image both bill ~1120 output tokens).
|
|
244
252
|
*
|
|
245
253
|
* Throws `ImageRefusalError` when Gemini blocked the drawing (see the class), and a
|
|
246
|
-
* plain `Error` naming
|
|
254
|
+
* plain `Error` naming the HTTP status, or the interaction status and error code, for
|
|
255
|
+
* anything else.
|
|
247
256
|
*/
|
|
248
257
|
declare function generateImage(apiKey: string, prompt: string, aspectRatio: ImageAspectRatio, opts?: GenerateImageOptions): Promise<GeneratedImage>;
|
|
249
258
|
|
package/dist/images.d.ts
CHANGED
|
@@ -216,19 +216,27 @@ interface GenerateImageOptions {
|
|
|
216
216
|
fetchImpl?: typeof fetch;
|
|
217
217
|
}
|
|
218
218
|
/**
|
|
219
|
-
* Gemini's Interactions API
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
219
|
+
* Gemini's Interactions API blocks a drawing in two shapes (both seen in production,
|
|
220
|
+
* 2026-10-13):
|
|
221
|
+
*
|
|
222
|
+
* - **HTTP 400** with `{"error": {"message": "Image generation blocked due to safety
|
|
223
|
+
* violations. Please modify your input and retry.", "code": "Unable to show the generated
|
|
224
|
+
* image. The image was filtered out because it violated Google's Generative AI Prohibited
|
|
225
|
+
* Use policy …"}}` — or "Request blocked due to prohibited content guidelines …". This is
|
|
226
|
+
* what an explicit prompt gets.
|
|
227
|
+
* - **HTTP 200** with no image block, `status: "failed" | "incomplete"` and an `errors[]`
|
|
228
|
+
* entry whose `code` names the block (https://ai.google.dev/gemini-api/docs/api-errors,
|
|
229
|
+
* "Generation blocked"): `image_safety`, `image_prohibited_content`, `image_recitation`,
|
|
230
|
+
* `image_other`, plus the text-side `safety`, `prohibited_content`, `recitation`,
|
|
231
|
+
* `blocklist`, `spii`, `content_blocked`. `no_image` is a plain generation failure.
|
|
225
232
|
*
|
|
226
233
|
* Deliberately NOT a `ModelRefusalError` subclass: this subpath is bundled apart from the
|
|
227
234
|
* main entry, so an `instanceof` against the main entry's class would never match. Hosts
|
|
228
235
|
* check `isImageRefusalError(error)` (by name) instead. Not retryable on the same prompt.
|
|
229
236
|
*/
|
|
230
237
|
declare class ImageRefusalError extends Error {
|
|
231
|
-
/** The block code as Gemini sent it (a URI or bare `snake_case`
|
|
238
|
+
/** The block code as Gemini sent it: an `errors[].code` (a URI or bare `snake_case`
|
|
239
|
+
* name) on the HTTP 200 shape, or `http_400` on the error-body shape. */
|
|
232
240
|
code: string;
|
|
233
241
|
/** Gemini's human-readable message, when it sent one. */
|
|
234
242
|
reason?: string;
|
|
@@ -243,7 +251,8 @@ declare function isImageRefusalError(error: unknown): error is ImageRefusalError
|
|
|
243
251
|
* IMAGE, not per pixel (a 1K and a 2K image both bill ~1120 output tokens).
|
|
244
252
|
*
|
|
245
253
|
* Throws `ImageRefusalError` when Gemini blocked the drawing (see the class), and a
|
|
246
|
-
* plain `Error` naming
|
|
254
|
+
* plain `Error` naming the HTTP status, or the interaction status and error code, for
|
|
255
|
+
* anything else.
|
|
247
256
|
*/
|
|
248
257
|
declare function generateImage(apiKey: string, prompt: string, aspectRatio: ImageAspectRatio, opts?: GenerateImageOptions): Promise<GeneratedImage>;
|
|
249
258
|
|
package/dist/images.js
CHANGED
|
@@ -262,7 +262,8 @@ function calculateImageCost(model, imageOutputTokens, textInputTokens) {
|
|
|
262
262
|
|
|
263
263
|
// src/images/google-image.ts
|
|
264
264
|
var ImageRefusalError = class extends Error {
|
|
265
|
-
/** The block code as Gemini sent it (a URI or bare `snake_case`
|
|
265
|
+
/** The block code as Gemini sent it: an `errors[].code` (a URI or bare `snake_case`
|
|
266
|
+
* name) on the HTTP 200 shape, or `http_400` on the error-body shape. */
|
|
266
267
|
code;
|
|
267
268
|
/** Gemini's human-readable message, when it sent one. */
|
|
268
269
|
reason;
|
|
@@ -279,6 +280,7 @@ function isImageRefusalError(error) {
|
|
|
279
280
|
return !!error && typeof error === "object" && error.name === "ImageRefusalError";
|
|
280
281
|
}
|
|
281
282
|
var BLOCK_CODE_MARKERS = ["safety", "prohibited", "recitation", "blocklist", "spii", "content_blocked", "image_other"];
|
|
283
|
+
var BLOCK_TEXT = /blocked|safety violation|prohibited|filtered out|prohibited use policy/i;
|
|
282
284
|
function describeBlockCode(code) {
|
|
283
285
|
const tail = code.split(/[#/]/).filter(Boolean).pop() ?? code;
|
|
284
286
|
return tail.toLowerCase();
|
|
@@ -287,6 +289,9 @@ function isBlockCode(code) {
|
|
|
287
289
|
const tail = describeBlockCode(code);
|
|
288
290
|
return BLOCK_CODE_MARKERS.some((marker) => tail.includes(marker));
|
|
289
291
|
}
|
|
292
|
+
function trimReason(text) {
|
|
293
|
+
return text.replace(/\s*\[[^\]]*\]\([^)]*\)[^.]*\.?/g, "").trim().slice(0, 300);
|
|
294
|
+
}
|
|
290
295
|
async function generateImage(apiKey, prompt, aspectRatio, opts = {}) {
|
|
291
296
|
const model = opts.model ?? DEFAULT_IMAGE_MODEL;
|
|
292
297
|
const input = [{ type: "text", text: prompt }];
|
|
@@ -305,7 +310,7 @@ async function generateImage(apiKey, prompt, aspectRatio, opts = {}) {
|
|
|
305
310
|
})
|
|
306
311
|
});
|
|
307
312
|
if (!res.ok) {
|
|
308
|
-
throw
|
|
313
|
+
throw httpError(model, res.status, await res.text());
|
|
309
314
|
}
|
|
310
315
|
const json = await res.json();
|
|
311
316
|
const b64 = (json.steps || []).flatMap((s) => s.content || []).find((c) => c.type === "image" && c.data)?.data;
|
|
@@ -315,16 +320,39 @@ async function generateImage(apiKey, prompt, aspectRatio, opts = {}) {
|
|
|
315
320
|
const inputTokens = usage.total_input_tokens || 0;
|
|
316
321
|
return { buffer: Buffer.from(b64, "base64"), costUSD: calculateImageCost(model, imageTokens, inputTokens) };
|
|
317
322
|
}
|
|
323
|
+
function httpError(model, status, body) {
|
|
324
|
+
if (status === 400) {
|
|
325
|
+
let error;
|
|
326
|
+
try {
|
|
327
|
+
error = JSON.parse(body)?.error;
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
const message = typeof error?.message === "string" ? error.message : "";
|
|
331
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
332
|
+
if (BLOCK_TEXT.test(message) || BLOCK_TEXT.test(code)) {
|
|
333
|
+
return new ImageRefusalError(model, "http_400", trimReason(message || code));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return new Error(`Image request failed: HTTP ${status} ${body.slice(0, 600)}`);
|
|
337
|
+
}
|
|
318
338
|
function imagelessReplyError(model, json) {
|
|
319
339
|
const errors = Array.isArray(json?.errors) ? json.errors : [];
|
|
320
340
|
const codes = errors.map((e) => String(e?.code ?? "")).filter(Boolean);
|
|
321
341
|
const messages = errors.map((e) => typeof e?.message === "string" ? e.message.trim() : "").filter(Boolean);
|
|
322
342
|
const block = codes.find(isBlockCode);
|
|
323
343
|
if (block) {
|
|
324
|
-
return new ImageRefusalError(model, block, messages[0]
|
|
344
|
+
return new ImageRefusalError(model, block, messages[0] ? trimReason(messages[0]) : void 0);
|
|
345
|
+
}
|
|
346
|
+
const text = (json?.steps || []).flatMap((s) => s.content || []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text.trim()).join(" ").slice(0, 300);
|
|
347
|
+
if (!codes.length && BLOCK_TEXT.test(text)) {
|
|
348
|
+
return new ImageRefusalError(model, "text_block", trimReason(text));
|
|
325
349
|
}
|
|
326
350
|
const status = typeof json?.status === "string" ? json.status : "unknown";
|
|
327
|
-
const detail = [
|
|
351
|
+
const detail = [
|
|
352
|
+
codes.length ? `code ${codes.map(describeBlockCode).join(", ")}` : "",
|
|
353
|
+
messages[0]?.slice(0, 300) ?? "",
|
|
354
|
+
text ? `text "${text}"` : ""
|
|
355
|
+
].filter(Boolean).join("; ");
|
|
328
356
|
return new Error(`Image response contained no image data (status ${status}${detail ? `; ${detail}` : ""})`);
|
|
329
357
|
}
|
|
330
358
|
|
package/dist/images.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/images/index.ts","../src/images/types.ts","../src/images/framing.ts","../src/images/sheet-detection.ts","../src/images/image-catalog.ts","../src/images/google-image.ts","../src/images/portrait-sheet.ts"],"sourcesContent":["/**\n * `@hiper2d/ai-agents/images` — image generation and portrait sheets.\n *\n * Kept off the main entry on purpose: this subpath is for hosts that draw pictures.\n * It never imports sharp (the host passes its own instance in), so nothing native\n * rides along, and the geometry helpers are pure and safe in a browser bundle.\n */\nexport type {\n ImageRect, ImageSize, AvatarCircle, AvatarFraming, ImageFocus,\n PortraitCell, PortraitSlice, DrawnSheet, GeneratedImage, SharpLike, SharpPipeline,\n} from './types';\nexport {\n CARD_ASPECT, CARD_WIDTH_PX, CARD_HEIGHT_PX, DEFAULT_AVATAR_CIRCLE, MIN_CARD_HEIGHT_FRACTION,\n} from './types';\n\nexport {\n cardInCell, defaultFraming, fitCircle, fitCard, fitFraming, isFramingShape,\n circleFocus, cardFocus, circleFocusOnSheet, focusToBackground,\n} from './framing';\n\nexport { findDividers, equalSplitGrid, detectSheetGrid, describeDividers } from './sheet-detection';\nexport type { GreyPlane, SheetGrid } from './sheet-detection';\n\nexport {\n IMAGE_MODEL_CONSTANTS, IMAGE_MODEL_PRICING, DEFAULT_IMAGE_MODEL, calculateImageCost,\n} from './image-catalog';\nexport type { ImageModelId, ImageModelPricing } from './image-catalog';\n\nexport { generateImage, ImageRefusalError, isImageRefusalError } from './google-image';\nexport type { GenerateImageOptions, ImageAspectRatio } from './google-image';\n\nexport {\n gridFor, padCells, buildPortraitSheetPrompt, sliceSheet, cutCard, drawPortraitSheet,\n DEFAULT_FILLER_PROMPT, SHEET_MAX_WIDTH, SHEET_JPEG_QUALITY, SHEET_MAX_BASE64_BYTES,\n} from './portrait-sheet';\nexport type {\n PortraitSheetPrompt, SheetMismatch, SliceSheetOptions,\n DrawPortraitSheetSpec, DrawPortraitSheetOptions, DrawnPortraitSheet,\n} from './portrait-sheet';\n","/**\n * Shared shapes for the portrait-sheet pipeline. Coordinates are SHEET pixels unless\n * stated otherwise; a card is portrait 3:4 and the circle inside it is card-relative.\n */\n\n/** A rectangle in image pixels (sharp's `extract` shape). */\nexport interface ImageRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport interface ImageSize {\n width: number;\n height: number;\n}\n\n/** The round avatar inside a card. `x` and `d` are fractions of the card's width,\n * `y` a fraction of its height. */\nexport interface AvatarCircle {\n x: number;\n y: number;\n d: number;\n}\n\n/** Where a portrait comes from on its sheet: the card cut and the circle in it. */\nexport interface AvatarFraming {\n card: ImageRect;\n circle: AvatarCircle;\n}\n\n/** The portion of an image to display, as fractions of its width/height. */\nexport interface ImageFocus {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n// Cards are portrait 3:4 (a poster's shape), stored at this size.\nexport const CARD_ASPECT = 3 / 4; // width / height\nexport const CARD_WIDTH_PX = 600;\nexport const CARD_HEIGHT_PX = 800;\n// Where the circle starts on a freshly cut card: 86% of its width, centred,\n// near the top — wide enough to take in a chin or beard (0.72 cut them off).\nexport const DEFAULT_AVATAR_CIRCLE: AvatarCircle = { x: 0.07, y: 0.02, d: 0.86 };\n// A card narrower than this fraction of the sheet's height upscales too much.\nexport const MIN_CARD_HEIGHT_FRACTION = 0.12;\n\n/** One character on a portrait sheet. */\nexport interface PortraitCell {\n /** Stable id the caller stores the crop under; never drawn into the image. */\n key: string;\n /** Character name, for logs and prompt guidance; never drawn into the image. */\n label: string;\n /** One-line visual description for this cell (gender, name, appearance). */\n prompt: string;\n}\n\n/** A card cut from a drawn sheet. */\nexport interface PortraitSlice {\n key: string;\n label: string;\n jpeg: Buffer;\n /** The framing the slicer chose: the card on the sheet and the default circle. */\n framing: AvatarFraming;\n}\n\n/** The kept sheet: the grid image itself (re-encoded), its size and the cells\n * the cards were cut from — enough to re-cut any card later at a new framing. */\nexport interface DrawnSheet {\n jpeg: Buffer;\n width: number;\n height: number;\n cells: ImageRect[];\n /** false = no divider lines were found and the cells are the equal split of the\n * requested grid; true = the cells between the lines the model actually drew. */\n detected: boolean;\n}\n\nexport interface GeneratedImage {\n buffer: Buffer;\n costUSD: number;\n}\n\n/**\n * The slice of sharp's API the pipeline uses. The library never imports sharp — a\n * native module the host installs and passes in (`(await import('sharp')).default`) —\n * so text-only consumers pull in nothing new.\n */\nexport type SharpLike = (input: Buffer) => {\n resize(options: { width: number; withoutEnlargement?: boolean }): SharpPipeline;\n resize(width: number, height: number): SharpPipeline;\n greyscale(): SharpPipeline;\n extract(rect: ImageRect): SharpPipeline;\n jpeg(options: { quality: number; mozjpeg?: boolean }): SharpPipeline;\n raw(): SharpPipeline;\n toBuffer(): Promise<Buffer>;\n toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: { width: number; height: number } }>;\n metadata(): Promise<{ width?: number; height?: number }>;\n};\n\nexport type SharpPipeline = ReturnType<SharpLike>;\n","import {\n AvatarCircle,\n AvatarFraming,\n CARD_ASPECT,\n DEFAULT_AVATAR_CIRCLE,\n ImageFocus,\n ImageRect,\n ImageSize,\n MIN_CARD_HEIGHT_FRACTION,\n} from './types';\n\n/**\n * Pure framing geometry, shared by the slicer (server), reframe validation (server)\n * and renderers/editors (browser). No I/O here.\n *\n * Coordinates: a card is an ImageRect in SHEET pixels (portrait 3:4); the circle is\n * card-relative (x, d = fractions of the card's width, y = fraction of its height).\n * An ImageFocus is what a renderer needs: the fraction of an image to show, so the\n * same avatar component can show a circle cut out of a stored card OR out of a\n * static preset sheet.\n */\n\nconst clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));\n\n/** Largest 3:4 card that fits a cell, centered horizontally and top-aligned:\n * portraits are busts with the head near the top, so the top edge is the\n * anchor that keeps the face in. */\nexport function cardInCell(cell: ImageRect): ImageRect {\n const width = Math.floor(Math.min(cell.width, cell.height * CARD_ASPECT));\n const height = Math.min(cell.height, Math.round(width / CARD_ASPECT));\n return {\n left: Math.round(cell.left + (cell.width - width) / 2),\n top: cell.top,\n width,\n height,\n };\n}\n\nexport function defaultFraming(cell: ImageRect): AvatarFraming {\n return { card: cardInCell(cell), circle: { ...DEFAULT_AVATAR_CIRCLE } };\n}\n\n/** Keeps the circle inside its card. */\nexport function fitCircle(circle: AvatarCircle): AvatarCircle {\n const d = clamp(circle.d, 0.15, 1);\n return {\n d,\n x: clamp(circle.x, 0, 1 - d),\n y: clamp(circle.y, 0, 1 - d * CARD_ASPECT),\n };\n}\n\n/** Snaps a card to a valid 3:4 rectangle inside the sheet, at least the\n * minimum size. Width is the free variable; height follows the aspect. */\nexport function fitCard(card: ImageRect, sheet: ImageSize): ImageRect {\n const minHeight = Math.max(16, Math.round(sheet.height * MIN_CARD_HEIGHT_FRACTION));\n const maxHeight = Math.min(sheet.height, sheet.width / CARD_ASPECT);\n const height = Math.round(clamp(card.height, minHeight, maxHeight));\n const width = Math.round(height * CARD_ASPECT);\n return {\n width,\n height,\n left: Math.round(clamp(card.left, 0, sheet.width - width)),\n top: Math.round(clamp(card.top, 0, sheet.height - height)),\n };\n}\n\nexport function fitFraming(framing: AvatarFraming, sheet: ImageSize): AvatarFraming {\n return { card: fitCard(framing.card, sheet), circle: fitCircle(framing.circle) };\n}\n\n/** True when the framing is a well-formed object with finite numbers. Shape\n * only — call fitFraming for the geometric bounds. */\nexport function isFramingShape(value: unknown): value is AvatarFraming {\n if (!value || typeof value !== 'object') return false;\n const f = value as any;\n const rect = f.card, circle = f.circle;\n const num = (v: unknown) => typeof v === 'number' && Number.isFinite(v);\n return !!rect && !!circle\n && num(rect.left) && num(rect.top) && num(rect.width) && num(rect.height)\n && rect.width > 0 && rect.height > 0\n && num(circle.x) && num(circle.y) && num(circle.d) && circle.d > 0;\n}\n\n/** The circle as a focus on the CARD image itself (a stored card cut). */\nexport function circleFocus(circle: AvatarCircle): ImageFocus {\n const c = fitCircle(circle);\n return { x: c.x, y: c.y, w: c.d, h: c.d * CARD_ASPECT };\n}\n\n/** The card as a focus on its sheet. */\nexport function cardFocus(card: ImageRect, sheet: ImageSize): ImageFocus {\n return {\n x: card.left / sheet.width,\n y: card.top / sheet.height,\n w: card.width / sheet.width,\n h: card.height / sheet.height,\n };\n}\n\n/** The circle as a focus on the SHEET (card and circle composed) — how a preset\n * mannequin renders straight off a static sheet. */\nexport function circleFocusOnSheet(framing: AvatarFraming, sheet: ImageSize): ImageFocus {\n const c = fitCircle(framing.circle);\n const card = framing.card;\n const side = c.d * card.width; // the circle is square, in sheet px\n return {\n x: (card.left + c.x * card.width) / sheet.width,\n y: (card.top + c.y * card.height) / sheet.height,\n w: side / sheet.width,\n h: side / sheet.height,\n };\n}\n\n/** CSS `background-size` / `background-position` that shows exactly `focus`\n * of an image inside a box. */\nexport function focusToBackground(focus: ImageFocus): { backgroundSize: string; backgroundPosition: string } {\n const axis = (pos: number, ext: number) => ext >= 1 ? 50 : (pos / (1 - ext)) * 100;\n return {\n backgroundSize: `${(100 / focus.w).toFixed(3)}% ${(100 / focus.h).toFixed(3)}%`,\n backgroundPosition: `${axis(focus.x, focus.w).toFixed(3)}% ${axis(focus.y, focus.h).toFixed(3)}%`,\n };\n}\n","import { ImageRect } from \"./types\";\n\n/**\n * Finds the cells of a drawn portrait sheet from its pixels.\n *\n * The prompt asks for equal cells separated by thin dark divider lines, but\n * the model draws the lines where it likes: on a correctly drawn 4x4 sheet\n * the rows measured 497 / 485 / 428 / 382 px where equal splits assumed 448\n * (2026-09-02), so fixed-interval slicing cut lower rows through the previous\n * character's chest. The lines themselves are crisp — near-black across the\n * full width — so they are easy to find, and the cells between them are the\n * real ones. Columns have always measured exact; rows drift.\n *\n * Pure over a greyscale byte plane so it is testable without sharp.\n */\n\nexport interface GreyPlane {\n width: number;\n height: number;\n // width*height bytes, row-major, 0 = black.\n data: Uint8Array;\n}\n\nexport interface SheetGrid {\n cells: ImageRect[]; // row-major\n cols: number;\n rows: number;\n // false = no usable divider lines were found and this is the equal split\n // of the REQUESTED grid. true = the cells between the lines the model\n // actually drew, which may be a different grid than requested (a 4x4\n // request has come back as 4x3 and as 6x3) — the sheet is the truth.\n detected: boolean;\n}\n\n// A pixel darker than this is \"line\"; a scanline with more than this share\n// of dark pixels is a divider. Cell backgrounds are flat muted colours, well\n// above the threshold, and the dark portrait clothing never spans a full line.\nconst DARK_MAX = 60;\nconst DIVIDER_SHARE = 0.85;\n// Pixels kept clear of a divider when cutting (anti-aliased edges).\nconst DIVIDER_INSET = 4;\n// A real divider is a thin line (~7 px on a 2400 px sheet). Thinner runs are\n// specks (a dark hairline inside a hooded figure once passed as a row line).\n// Thicker runs up to a small share of the sheet are still boundaries: the\n// model sometimes adds a dark name plate under each portrait despite the\n// no-text rule, and the plate belongs OUTSIDE the cell. Beyond that share a\n// dark run is a dark art style's background, not a boundary.\nconst DIVIDER_MIN_THICKNESS = 3;\nconst DIVIDER_MAX_THICKNESS_SHARE = 0.08;\n// A span between lines narrower than this share of the sheet can't hold a\n// face; the line that would create it is not a cell boundary.\nconst MIN_SPAN_SHARE = 0.12;\n\ninterface Run { start: number; end: number }\n\nfunction runs(indices: number[]): Run[] {\n const out: Run[] = [];\n for (const i of indices) {\n const last = out[out.length - 1];\n if (last && i === last.end + 1) last.end = i;\n else out.push({start: i, end: i});\n }\n return out;\n}\n\n/** Dark runs along one axis, measured over a window of the other axis (a\n * band of rows for columns, the full height by default). Border runs and\n * runs that aren't line-shaped are dropped. */\nfunction lineRuns(plane: GreyPlane, axis: 'rows' | 'cols', from: number, to: number): Run[] {\n const {width, height, data} = plane;\n const extent = axis === 'rows' ? height : width;\n const dark: number[] = [];\n for (let i = 0; i < extent; i++) {\n let count = 0;\n if (axis === 'rows') {\n const base = i * width;\n for (let x = from; x < to; x++) if (data[base + x] < DARK_MAX) count++;\n } else {\n for (let y = from; y < to; y++) if (data[y * width + i] < DARK_MAX) count++;\n }\n if (count / (to - from) > DIVIDER_SHARE) dark.push(i);\n }\n const maxThickness = extent * DIVIDER_MAX_THICKNESS_SHARE;\n return runs(dark)\n .filter(r => r.start > 2 && r.end < extent - 3)\n .filter(r => { const t = r.end - r.start + 1; return t >= DIVIDER_MIN_THICKNESS && t <= maxThickness; });\n}\n\n/** Lines that would leave a sliver on either side are not boundaries. */\nfunction dropSliverMakers(lines: Run[], extent: number): Run[] {\n const minSpan = extent * MIN_SPAN_SHARE;\n const kept: Run[] = [];\n let cursor = 0;\n for (const line of lines) {\n if (line.start - cursor < minSpan || extent - (line.end + 1) < minSpan) continue;\n kept.push(line);\n cursor = line.end + 1;\n }\n return kept;\n}\n\n/** Divider runs across the whole plane: row lines over the full width,\n * column lines over the full height. */\nexport function findDividers(plane: GreyPlane): {rows: Run[]; cols: Run[]} {\n return {\n rows: dropSliverMakers(lineRuns(plane, 'rows', 0, plane.width), plane.height),\n cols: dropSliverMakers(lineRuns(plane, 'cols', 0, plane.height), plane.width),\n };\n}\n\n/** Cells between dividers along one axis, from the border to the first line,\n * line to line, last line to the border. */\nfunction spans(dividers: Run[], extent: number): {start: number; end: number}[] {\n const out: {start: number; end: number}[] = [];\n let cursor = 0;\n for (const d of dividers) {\n out.push({start: cursor, end: d.start});\n cursor = d.end + 1;\n }\n out.push({start: cursor, end: extent});\n return out;\n}\n\nexport function equalSplitGrid(width: number, height: number, cols: number, rows: number): SheetGrid {\n const cellW = Math.floor(width / cols), cellH = Math.floor(height / rows);\n const cells: ImageRect[] = [];\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n cells.push(inset({left: c * cellW, top: r * cellH, width: cellW, height: cellH}, 8));\n }\n }\n return {cells, cols, rows, detected: false};\n}\n\nfunction inset(rect: ImageRect, by: number): ImageRect {\n return {left: rect.left + by, top: rect.top + by, width: Math.max(1, rect.width - 2 * by), height: Math.max(1, rect.height - 2 * by)};\n}\n\n/**\n * The sheet's cells. Read off the divider lines the model drew whenever they\n * form a plausible grid, even when that grid differs from the cols x rows\n * that were asked for — the model keeps row-major order, so the drawn cells\n * still map to the characters in sequence. Row lines are found over the\n * full width; column lines are found PER ROW BAND, because the model has\n * drawn rows with different column layouts (and a column line that stops\n * short of the last row). A band without lines of its own borrows the\n * layout of the band above it. The equal split of the requested grid is the\n * last resort when no lines are found at all (the caller logs it; the kept\n * sheet lets the owner reframe by hand either way).\n */\nexport function detectSheetGrid(plane: GreyPlane, cols: number, rows: number): SheetGrid {\n const {width, height} = plane;\n const rowLines = dropSliverMakers(lineRuns(plane, 'rows', 0, width), height);\n const bands = spans(rowLines, height);\n const cells: ImageRect[] = [];\n let previous: Run[] | null = null;\n let colCount = 0;\n for (const band of bands) {\n let colLines = dropSliverMakers(lineRuns(plane, 'cols', band.start, band.end), width);\n if (colLines.length === 0 && previous) colLines = previous;\n previous = colLines;\n const xs = spans(colLines, width);\n colCount = Math.max(colCount, xs.length);\n for (const x of xs) {\n cells.push(inset({left: x.start, top: band.start, width: x.end - x.start, height: band.end - band.start}, DIVIDER_INSET));\n }\n }\n if (colCount < 2) return equalSplitGrid(width, height, cols, rows);\n return {cells, cols: colCount, rows: bands.length, detected: true};\n}\n\n/** What findDividers saw, for the mismatch log line. */\nexport function describeDividers(plane: GreyPlane): string {\n const d = findDividers(plane);\n const fmt = (r: Run[]) => r.map(x => `${x.start}-${x.end}`).join(',') || 'none';\n return `rows@${fmt(d.rows)} cols@${fmt(d.cols)}`;\n}\n","/**\n * Image-generation models: platform-side, never user-selected. Prices per 1M tokens\n * from ai.google.dev/gemini-api/docs/pricing (2026-08). Image output bills ~1120 tokens\n * per image regardless of resolution, so one image ≈ $0.067 — fewer calls, not lower\n * resolution, is what minimizes cost.\n */\nexport const IMAGE_MODEL_CONSTANTS = {\n GEMINI_FLASH_IMAGE: 'gemini-3.1-flash-image',\n} as const;\n\nexport type ImageModelId = typeof IMAGE_MODEL_CONSTANTS[keyof typeof IMAGE_MODEL_CONSTANTS];\n\nexport interface ImageModelPricing {\n imageOutputPricePerM: number;\n textInputPricePerM: number;\n}\n\nexport const IMAGE_MODEL_PRICING: Record<ImageModelId, ImageModelPricing> = {\n [IMAGE_MODEL_CONSTANTS.GEMINI_FLASH_IMAGE]: {\n imageOutputPricePerM: 60,\n textInputPricePerM: 0.5,\n },\n};\n\nexport const DEFAULT_IMAGE_MODEL: ImageModelId = IMAGE_MODEL_CONSTANTS.GEMINI_FLASH_IMAGE;\n\n/** Cost of one image call from the usage Gemini reports. */\nexport function calculateImageCost(model: ImageModelId, imageOutputTokens: number, textInputTokens: number): number {\n const pricing = IMAGE_MODEL_PRICING[model];\n return parseFloat((\n imageOutputTokens / 1_000_000 * pricing.imageOutputPricePerM +\n textInputTokens / 1_000_000 * pricing.textInputPricePerM\n ).toFixed(6));\n}\n","import { calculateImageCost, DEFAULT_IMAGE_MODEL, ImageModelId } from './image-catalog';\nimport { GeneratedImage } from './types';\n\nexport type ImageAspectRatio = '1:1' | '3:4' | '4:3' | '3:2' | '2:3' | '16:9' | '9:16';\n\nexport interface GenerateImageOptions {\n /** Labeled reference JPEGs (an established scene, character portraits) that\n * anchor the drawing to a known style, place or faces. */\n references?: { label: string; jpeg: Buffer }[];\n imageSize?: '1K' | '2K';\n model?: ImageModelId;\n /** Test seam / custom transport; defaults to the global fetch. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Gemini's Interactions API answers a blocked image with HTTP 200, no image block,\n * `status: \"failed\"` (or `incomplete`) and an `errors[]` entry whose `code` names the\n * block (https://ai.google.dev/gemini-api/docs/api-errors, \"Generation blocked\"):\n * `image_safety`, `image_prohibited_content`, `image_recitation`, `image_other`, plus\n * the text-side codes `safety`, `prohibited_content`, `recitation`, `blocklist`, `spii`,\n * `content_blocked`. `no_image` is a plain generation failure, not a block.\n *\n * Deliberately NOT a `ModelRefusalError` subclass: this subpath is bundled apart from the\n * main entry, so an `instanceof` against the main entry's class would never match. Hosts\n * check `isImageRefusalError(error)` (by name) instead. Not retryable on the same prompt.\n */\nexport class ImageRefusalError extends Error {\n /** The block code as Gemini sent it (a URI or bare `snake_case` name). */\n public code: string;\n /** Gemini's human-readable message, when it sent one. */\n public reason?: string;\n public model: string;\n\n constructor(model: string, code: string, reason?: string) {\n super(`${model} declined to draw the image (${describeBlockCode(code)})${reason ? `: ${reason}` : ''}`);\n this.name = 'ImageRefusalError';\n this.model = model;\n this.code = code;\n this.reason = reason;\n }\n}\n\n/** Works across bundle copies and serialization boundaries (checks the name, not the class). */\nexport function isImageRefusalError(error: unknown): error is ImageRefusalError {\n return !!error && typeof error === 'object' && (error as { name?: unknown }).name === 'ImageRefusalError';\n}\n\nconst BLOCK_CODE_MARKERS = ['safety', 'prohibited', 'recitation', 'blocklist', 'spii', 'content_blocked', 'image_other'];\n\n/** The trailing identifier of a code URI such as `https://…/api-errors#image_safety`. */\nfunction describeBlockCode(code: string): string {\n const tail = code.split(/[#/]/).filter(Boolean).pop() ?? code;\n return tail.toLowerCase();\n}\n\nfunction isBlockCode(code: string): boolean {\n const tail = describeBlockCode(code);\n return BLOCK_CODE_MARKERS.some(marker => tail.includes(marker));\n}\n\n/**\n * One image-model call on Gemini's Interactions API. Pure — no auth, no billing —\n * and the result reports its cost so the host decides whom to charge. Cost is per\n * IMAGE, not per pixel (a 1K and a 2K image both bill ~1120 output tokens).\n *\n * Throws `ImageRefusalError` when Gemini blocked the drawing (see the class), and a\n * plain `Error` naming Gemini's status and error code for any other imageless reply.\n */\nexport async function generateImage(apiKey: string, prompt: string, aspectRatio: ImageAspectRatio, opts: GenerateImageOptions = {}): Promise<GeneratedImage> {\n const model = opts.model ?? DEFAULT_IMAGE_MODEL;\n const input: any[] = [{ type: 'text', text: prompt }];\n for (const ref of opts.references ?? []) {\n input.push({ type: 'text', text: ref.label });\n input.push({ type: 'image', mime_type: 'image/jpeg', data: ref.jpeg.toString('base64') });\n }\n const doFetch = opts.fetchImpl ?? fetch;\n const res = await doFetch('https://generativelanguage.googleapis.com/v1beta/interactions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },\n body: JSON.stringify({\n model,\n input,\n response_format: { type: 'image', mime_type: 'image/jpeg', aspect_ratio: aspectRatio, image_size: opts.imageSize ?? '2K' },\n }),\n });\n if (!res.ok) {\n throw new Error(`Image request failed: HTTP ${res.status} ${(await res.text()).slice(0, 300)}`);\n }\n const json: any = await res.json();\n const b64 = (json.steps || [])\n .flatMap((s: any) => s.content || [])\n .find((c: any) => c.type === 'image' && c.data)?.data;\n if (!b64) throw imagelessReplyError(model, json);\n\n const usage = json.usage || {};\n const imageTokens = (usage.output_tokens_by_modality || [])\n .filter((m: any) => m.modality === 'image')\n .reduce((sum: number, m: any) => sum + (m.tokens || 0), 0);\n const inputTokens = usage.total_input_tokens || 0;\n\n return { buffer: Buffer.from(b64, 'base64'), costUSD: calculateImageCost(model, imageTokens, inputTokens) };\n}\n\n/** Classifies an HTTP 200 reply that carried no image block. */\nfunction imagelessReplyError(model: string, json: any): Error {\n const errors: { code?: unknown; message?: unknown }[] = Array.isArray(json?.errors) ? json.errors : [];\n const codes = errors.map(e => String(e?.code ?? '')).filter(Boolean);\n const messages = errors.map(e => (typeof e?.message === 'string' ? e.message.trim() : '')).filter(Boolean);\n const block = codes.find(isBlockCode);\n if (block) {\n return new ImageRefusalError(model, block, messages[0]?.slice(0, 300));\n }\n const status = typeof json?.status === 'string' ? json.status : 'unknown';\n const detail = [codes.length ? `code ${codes.map(describeBlockCode).join(', ')}` : '', messages[0]?.slice(0, 300) ?? '']\n .filter(Boolean)\n .join(': ');\n return new Error(`Image response contained no image data (status ${status}${detail ? `; ${detail}` : ''})`);\n}\n","import { generateImage, GenerateImageOptions } from './google-image';\nimport { defaultFraming } from './framing';\nimport { describeDividers, detectSheetGrid, equalSplitGrid } from './sheet-detection';\nimport { CARD_HEIGHT_PX, CARD_WIDTH_PX, DrawnSheet, ImageRect, PortraitCell, PortraitSlice, SharpLike } from './types';\n\n/**\n * Portrait sheets: one image-model call draws a whole cast as a grid of bust\n * portraits, and the grid is cut into one card per character. One call costs the\n * same whether it draws one face or sixteen, so this is the cheap way to give\n * every character in a game a portrait in a single consistent style.\n *\n * Nothing here touches storage or billing: the host stores the cards and the kept\n * sheet (so a card can be re-cut later at a new framing) and charges whoever it\n * likes for `costUSD`. sharp is passed in, never imported.\n */\n\n/**\n * Grid dimensions by cell count on the 4:3 canvas. Cells must come out\n * square-to-portrait: asked for 4x4 (landscape 600x448 cells) the model redrew the\n * sheet as 4x3, 6x3 and once as an irregular two-layout sheet — every time towards\n * taller cells. 5x3 keeps 480x597 cells for 13-15; 17 cells get 6x3 (400x600, 2:3,\n * one of the shapes the model volunteered). Never 4x4: it silently cut a 17-cell\n * cast to 16.\n */\nexport function gridFor(cells: number): { cols: number; rows: number } {\n if (cells <= 6) return { cols: 3, rows: 2 };\n if (cells <= 8) return { cols: 4, rows: 2 };\n if (cells <= 9) return { cols: 3, rows: 3 };\n if (cells <= 12) return { cols: 4, rows: 3 };\n if (cells <= 15) return { cols: 5, rows: 3 };\n if (cells <= 18) return { cols: 6, rows: 3 };\n throw new Error(`Portrait sheet cannot hold ${cells} cells (max 18)`);\n}\n\nexport const DEFAULT_FILLER_PROMPT = `\"Stranger\" — an anonymous hooded figure fitting the setting, face hidden in shadow`;\n\n/**\n * Pads a cast out to a full grid: the model draws exactly cols x rows cells, so the\n * spare ones get an anonymous filler that is drawn and discarded.\n */\nexport function padCells(cells: PortraitCell[], fillerPrompt: string = DEFAULT_FILLER_PROMPT): { cells: PortraitCell[]; cols: number; rows: number; realCount: number } {\n const { cols, rows } = gridFor(cells.length);\n const padded = [...cells];\n for (let i = padded.length; i < cols * rows; i++) {\n padded.push({ key: `__filler${i}`, label: 'Stranger', prompt: fillerPrompt });\n }\n return { cells: padded, cols, rows, realCount: cells.length };\n}\n\nexport interface PortraitSheetPrompt {\n cells: PortraitCell[];\n cols: number;\n rows: number;\n /** What the sheet is for, e.g. \"a social deduction game\" — one noun phrase. */\n purpose: string;\n /** The world the characters live in: a title and a short description. */\n setting: { title: string; description: string };\n /** Player-chosen art direction, already sanitized by the host. Replaces the\n * model's free choice of style. */\n artStyle?: string;\n}\n\n/**\n * The sheet prompt. The wording is load-bearing: equal cells with thin dark\n * divider lines are what the slicer finds, one flat muted background per cell keeps\n * the lines detectable in any art style, and the no-text rule is repeated because\n * the model otherwise typesets names and bios into the image.\n */\nexport function buildPortraitSheetPrompt(spec: PortraitSheetPrompt): string {\n const { cells, cols, rows, purpose, setting, artStyle } = spec;\n const cellLines = cells.map(\n (c, i) => `Cell ${i + 1}: ${c.prompt}. Its own distinct flat solid muted background color.`\n ).join('\\n');\n\n // A player style leads the prompt and is restated at the end: the sheet\n // framing (grid, bust portraits, flat backgrounds) otherwise pulls the model\n // toward drawn character-sheet art whatever style was asked for (an\n // \"ultra-realism\" game came out as comic portraits). The grid wording stays\n // intact — it is what the slicer relies on — but is declared layout only.\n const styleLead = artStyle\n ? `Art style, chosen by the player — it governs every portrait: \"${artStyle}\". Each cell must look made in that style: a photographic or cinematic style means real photographs, a painted style means paintings.\\n\\n`\n : '';\n const styleLine = artStyle\n ? `Render every portrait in the player's art style above. Apply it consistently to every portrait: same rendering technique, same palette family, same lighting.`\n : `Choose ONE cohesive illustration style that fits this setting and apply it consistently to every portrait: same rendering technique, same palette family, same lighting.`;\n const styleTail = artStyle ? `\\n\\nEvery cell must be fully in this art style: \"${artStyle}\" — and still carry no names or text of any kind.` : '';\n\n return `${styleLead}A character portrait sheet for ${purpose}, rendered as a single image: a precise grid of exactly ${cells.length} rectangular cells, ${cols} columns and ${rows} rows, all cells exactly equal size, separated by thin dark divider lines. Each cell contains one bust portrait (head and shoulders) of a different character, centered in its cell.\n\nSetting — \"${setting.title}\": ${setting.description}\n\n${styleLine} Every face must be distinct and memorable, and match its character description. No character may span more than one cell. Give each cell its own flat solid muted desaturated background color, different from its neighbors. Row-major order, left to right, top to bottom:\n\n${cellLines}\n\nThe character descriptions above are guidance for the drawing only — NEVER render them as text. Absolutely no text anywhere in the image: no names, no labels, no captions, no letters, no writing of any kind — and no lettering on clothing, equipment, insignia or logos.${styleTail}`;\n}\n\n// The model returns ~2.4 MB loosely-compressed JPEGs; at this width and quality a\n// sheet is ~400 KB (~530 KB as base64), under a 1 MiB document limit with cells\n// still ~500 px tall.\nexport const SHEET_MAX_WIDTH = 2400;\nexport const SHEET_JPEG_QUALITY = 85;\nexport const SHEET_MAX_BASE64_BYTES = 900_000;\n\n/** Why a sheet's cells did not come out as requested; the host decides how loudly to log. */\nexport interface SheetMismatch {\n kind: 'no-dividers' | 'different-grid';\n message: string;\n detail: Record<string, unknown>;\n}\n\nexport interface SliceSheetOptions {\n onMismatch?: (mismatch: SheetMismatch) => void;\n /** Long edge the sheet is normalized to before cutting; defaults to SHEET_MAX_WIDTH. */\n maxWidth?: number;\n}\n\n/**\n * Cuts the cards out of a drawn sheet and keeps the sheet. Cells come from the\n * divider lines the model drew (equal split as a last resort); each card is the\n * largest 3:4 rectangle in its cell, top-anchored, with the default circle — the\n * framing the host can let a user move later.\n */\nexport async function sliceSheet(\n sharp: SharpLike,\n raw: Buffer,\n cells: PortraitCell[],\n count: number,\n cols: number,\n rows: number,\n opts: SliceSheetOptions = {},\n): Promise<{ slices: PortraitSlice[]; sheet: DrawnSheet }> {\n // Normalise the working resolution first so cells, cards and the stored sheet\n // all share one pixel space.\n const grid: Buffer = await sharp(raw).resize({ width: opts.maxWidth ?? SHEET_MAX_WIDTH, withoutEnlargement: true }).toBuffer();\n const { data, info } = await sharp(grid).greyscale().raw().toBuffer({ resolveWithObject: true });\n const width = info.width, height = info.height;\n if (width < 100 * cols || height < 100 * rows) throw new Error(`Portrait sheet has unusable dimensions ${width}x${height}`);\n\n const plane = { width, height, data: new Uint8Array(data.buffer, data.byteOffset, data.length) };\n const gridCells = detectSheetGrid(plane, cols, rows);\n if (!gridCells.detected) {\n opts.onMismatch?.({\n kind: 'no-dividers',\n message: `no divider lines found on the ${cols}x${rows} sheet; using equal split`,\n detail: { width, height, dividers: describeDividers(plane) },\n });\n } else if (gridCells.cols !== cols || gridCells.rows !== rows) {\n // The model drew a different grid than asked. Its cells are still in row-major\n // order, so they are used as drawn; only characters past the last drawn cell\n // fall back to the equal split.\n opts.onMismatch?.({\n kind: 'different-grid',\n message: `sheet drawn as ${gridCells.cols}x${gridCells.rows}, requested ${cols}x${rows}; using the drawn cells`,\n detail: { width, height, drawnCells: gridCells.cells.length, needed: count },\n });\n }\n const fallback = equalSplitGrid(width, height, cols, rows).cells;\n\n const slices: PortraitSlice[] = [];\n for (let i = 0; i < count; i++) {\n const framing = defaultFraming(gridCells.cells[i] ?? fallback[i]);\n const jpeg = await cutCard(sharp, grid, framing.card);\n slices.push({ key: cells[i].key, label: cells[i].label, jpeg, framing });\n }\n\n let sheetJpeg: Buffer = await sharp(grid).jpeg({ quality: SHEET_JPEG_QUALITY, mozjpeg: true }).toBuffer();\n if (sheetJpeg.length * 4 / 3 > SHEET_MAX_BASE64_BYTES) {\n sheetJpeg = await sharp(grid).jpeg({ quality: 72, mozjpeg: true }).toBuffer();\n }\n return {\n slices,\n sheet: { jpeg: sheetJpeg, width, height, cells: gridCells.cells.slice(0, count), detected: gridCells.detected },\n };\n}\n\n/** Cuts one card out of a stored sheet at the given framing. */\nexport async function cutCard(sharp: SharpLike, sheet: Buffer, card: ImageRect): Promise<Buffer> {\n return sharp(sheet)\n .extract(card)\n .resize(CARD_WIDTH_PX, CARD_HEIGHT_PX)\n .jpeg({ quality: 85 })\n .toBuffer();\n}\n\nexport interface DrawPortraitSheetSpec extends Omit<PortraitSheetPrompt, 'cols' | 'rows'> {\n /** Prompt for the cells that pad the cast out to a full grid. */\n fillerPrompt?: string;\n}\n\nexport interface DrawPortraitSheetOptions extends SliceSheetOptions {\n image?: Pick<GenerateImageOptions, 'model' | 'imageSize' | 'fetchImpl'>;\n}\n\nexport interface DrawnPortraitSheet {\n portraits: PortraitSlice[];\n sheet: DrawnSheet;\n /** What the image call cost, reported for the host's billing. */\n costUSD: number;\n}\n\n/**\n * The whole pipeline: lay the cast out on a grid, draw it in one call, cut the\n * cards. Filler cells are drawn and discarded; `portraits` has exactly one entry\n * per input cell, in order.\n */\nexport async function drawPortraitSheet(apiKey: string, sharp: SharpLike, spec: DrawPortraitSheetSpec, opts: DrawPortraitSheetOptions = {}): Promise<DrawnPortraitSheet> {\n const { cells, cols, rows, realCount } = padCells(spec.cells, spec.fillerPrompt);\n const prompt = buildPortraitSheetPrompt({ cells, cols, rows, purpose: spec.purpose, setting: spec.setting, artStyle: spec.artStyle });\n const image = await generateImage(apiKey, prompt, '4:3', opts.image);\n const { slices, sheet } = await sliceSheet(sharp, image.buffer, cells, realCount, cols, rows, opts);\n return { portraits: slices, sheet, costUSD: image.costUSD };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACyCO,IAAM,cAAc,IAAI;AACxB,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AAGvB,IAAM,wBAAsC,EAAE,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK;AAExE,IAAM,2BAA2B;;;AC1BxC,IAAM,QAAQ,CAAC,GAAW,KAAa,QAAgB,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC;AAK9E,SAAS,WAAW,MAA4B;AACnD,QAAM,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,WAAW,CAAC;AACxE,QAAM,SAAS,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM,QAAQ,WAAW,CAAC;AACpE,SAAO;AAAA,IACH,MAAM,KAAK,MAAM,KAAK,QAAQ,KAAK,QAAQ,SAAS,CAAC;AAAA,IACrD,KAAK,KAAK;AAAA,IACV;AAAA,IACA;AAAA,EACJ;AACJ;AAEO,SAAS,eAAe,MAAgC;AAC3D,SAAO,EAAE,MAAM,WAAW,IAAI,GAAG,QAAQ,EAAE,GAAG,sBAAsB,EAAE;AAC1E;AAGO,SAAS,UAAU,QAAoC;AAC1D,QAAM,IAAI,MAAM,OAAO,GAAG,MAAM,CAAC;AACjC,SAAO;AAAA,IACH;AAAA,IACA,GAAG,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC;AAAA,IAC3B,GAAG,MAAM,OAAO,GAAG,GAAG,IAAI,IAAI,WAAW;AAAA,EAC7C;AACJ;AAIO,SAAS,QAAQ,MAAiB,OAA6B;AAClE,QAAM,YAAY,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,SAAS,wBAAwB,CAAC;AAClF,QAAM,YAAY,KAAK,IAAI,MAAM,QAAQ,MAAM,QAAQ,WAAW;AAClE,QAAM,SAAS,KAAK,MAAM,MAAM,KAAK,QAAQ,WAAW,SAAS,CAAC;AAClE,QAAM,QAAQ,KAAK,MAAM,SAAS,WAAW;AAC7C,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,MAAM,KAAK,MAAM,MAAM,KAAK,MAAM,GAAG,MAAM,QAAQ,KAAK,CAAC;AAAA,IACzD,KAAK,KAAK,MAAM,MAAM,KAAK,KAAK,GAAG,MAAM,SAAS,MAAM,CAAC;AAAA,EAC7D;AACJ;AAEO,SAAS,WAAW,SAAwB,OAAiC;AAChF,SAAO,EAAE,MAAM,QAAQ,QAAQ,MAAM,KAAK,GAAG,QAAQ,UAAU,QAAQ,MAAM,EAAE;AACnF;AAIO,SAAS,eAAe,OAAwC;AACnE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,IAAI;AACV,QAAM,OAAO,EAAE,MAAM,SAAS,EAAE;AAChC,QAAM,MAAM,CAAC,MAAe,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC;AACtE,SAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,UACZ,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,MAAM,KACrE,KAAK,QAAQ,KAAK,KAAK,SAAS,KAChC,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,OAAO,IAAI;AACzE;AAGO,SAAS,YAAY,QAAkC;AAC1D,QAAM,IAAI,UAAU,MAAM;AAC1B,SAAO,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,IAAI,YAAY;AAC1D;AAGO,SAAS,UAAU,MAAiB,OAA8B;AACrE,SAAO;AAAA,IACH,GAAG,KAAK,OAAO,MAAM;AAAA,IACrB,GAAG,KAAK,MAAM,MAAM;AAAA,IACpB,GAAG,KAAK,QAAQ,MAAM;AAAA,IACtB,GAAG,KAAK,SAAS,MAAM;AAAA,EAC3B;AACJ;AAIO,SAAS,mBAAmB,SAAwB,OAA8B;AACrF,QAAM,IAAI,UAAU,QAAQ,MAAM;AAClC,QAAM,OAAO,QAAQ;AACrB,QAAM,OAAO,EAAE,IAAI,KAAK;AACxB,SAAO;AAAA,IACH,IAAI,KAAK,OAAO,EAAE,IAAI,KAAK,SAAS,MAAM;AAAA,IAC1C,IAAI,KAAK,MAAM,EAAE,IAAI,KAAK,UAAU,MAAM;AAAA,IAC1C,GAAG,OAAO,MAAM;AAAA,IAChB,GAAG,OAAO,MAAM;AAAA,EACpB;AACJ;AAIO,SAAS,kBAAkB,OAA2E;AACzG,QAAM,OAAO,CAAC,KAAa,QAAgB,OAAO,IAAI,KAAM,OAAO,IAAI,OAAQ;AAC/E,SAAO;AAAA,IACH,gBAAgB,IAAI,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC;AAAA,IAC5E,oBAAoB,GAAG,KAAK,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EAClG;AACJ;;;ACrFA,IAAM,WAAW;AACjB,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAOtB,IAAM,wBAAwB;AAC9B,IAAM,8BAA8B;AAGpC,IAAM,iBAAiB;AAIvB,SAAS,KAAK,SAA0B;AACpC,QAAM,MAAa,CAAC;AACpB,aAAW,KAAK,SAAS;AACrB,UAAM,OAAO,IAAI,IAAI,SAAS,CAAC;AAC/B,QAAI,QAAQ,MAAM,KAAK,MAAM,EAAG,MAAK,MAAM;AAAA,QACtC,KAAI,KAAK,EAAC,OAAO,GAAG,KAAK,EAAC,CAAC;AAAA,EACpC;AACA,SAAO;AACX;AAKA,SAAS,SAAS,OAAkB,MAAuB,MAAc,IAAmB;AACxF,QAAM,EAAC,OAAO,QAAQ,KAAI,IAAI;AAC9B,QAAM,SAAS,SAAS,SAAS,SAAS;AAC1C,QAAM,OAAiB,CAAC;AACxB,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC7B,QAAI,QAAQ;AACZ,QAAI,SAAS,QAAQ;AACjB,YAAM,OAAO,IAAI;AACjB,eAAS,IAAI,MAAM,IAAI,IAAI,IAAK,KAAI,KAAK,OAAO,CAAC,IAAI,SAAU;AAAA,IACnE,OAAO;AACH,eAAS,IAAI,MAAM,IAAI,IAAI,IAAK,KAAI,KAAK,IAAI,QAAQ,CAAC,IAAI,SAAU;AAAA,IACxE;AACA,QAAI,SAAS,KAAK,QAAQ,cAAe,MAAK,KAAK,CAAC;AAAA,EACxD;AACA,QAAM,eAAe,SAAS;AAC9B,SAAO,KAAK,IAAI,EACX,OAAO,OAAK,EAAE,QAAQ,KAAK,EAAE,MAAM,SAAS,CAAC,EAC7C,OAAO,OAAK;AAAE,UAAM,IAAI,EAAE,MAAM,EAAE,QAAQ;AAAG,WAAO,KAAK,yBAAyB,KAAK;AAAA,EAAc,CAAC;AAC/G;AAGA,SAAS,iBAAiB,OAAc,QAAuB;AAC3D,QAAM,UAAU,SAAS;AACzB,QAAM,OAAc,CAAC;AACrB,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACtB,QAAI,KAAK,QAAQ,SAAS,WAAW,UAAU,KAAK,MAAM,KAAK,QAAS;AACxE,SAAK,KAAK,IAAI;AACd,aAAS,KAAK,MAAM;AAAA,EACxB;AACA,SAAO;AACX;AAIO,SAAS,aAAa,OAA8C;AACvE,SAAO;AAAA,IACH,MAAM,iBAAiB,SAAS,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG,MAAM,MAAM;AAAA,IAC5E,MAAM,iBAAiB,SAAS,OAAO,QAAQ,GAAG,MAAM,MAAM,GAAG,MAAM,KAAK;AAAA,EAChF;AACJ;AAIA,SAAS,MAAM,UAAiB,QAAgD;AAC5E,QAAM,MAAsC,CAAC;AAC7C,MAAI,SAAS;AACb,aAAW,KAAK,UAAU;AACtB,QAAI,KAAK,EAAC,OAAO,QAAQ,KAAK,EAAE,MAAK,CAAC;AACtC,aAAS,EAAE,MAAM;AAAA,EACrB;AACA,MAAI,KAAK,EAAC,OAAO,QAAQ,KAAK,OAAM,CAAC;AACrC,SAAO;AACX;AAEO,SAAS,eAAe,OAAe,QAAgB,MAAc,MAAyB;AACjG,QAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI,GAAG,QAAQ,KAAK,MAAM,SAAS,IAAI;AACxE,QAAM,QAAqB,CAAC;AAC5B,WAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC3B,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC3B,YAAM,KAAK,MAAM,EAAC,MAAM,IAAI,OAAO,KAAK,IAAI,OAAO,OAAO,OAAO,QAAQ,MAAK,GAAG,CAAC,CAAC;AAAA,IACvF;AAAA,EACJ;AACA,SAAO,EAAC,OAAO,MAAM,MAAM,UAAU,MAAK;AAC9C;AAEA,SAAS,MAAM,MAAiB,IAAuB;AACnD,SAAO,EAAC,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,QAAQ,IAAI,EAAE,GAAG,QAAQ,KAAK,IAAI,GAAG,KAAK,SAAS,IAAI,EAAE,EAAC;AACxI;AAcO,SAAS,gBAAgB,OAAkB,MAAc,MAAyB;AACrF,QAAM,EAAC,OAAO,OAAM,IAAI;AACxB,QAAM,WAAW,iBAAiB,SAAS,OAAO,QAAQ,GAAG,KAAK,GAAG,MAAM;AAC3E,QAAM,QAAQ,MAAM,UAAU,MAAM;AACpC,QAAM,QAAqB,CAAC;AAC5B,MAAI,WAAyB;AAC7B,MAAI,WAAW;AACf,aAAW,QAAQ,OAAO;AACtB,QAAI,WAAW,iBAAiB,SAAS,OAAO,QAAQ,KAAK,OAAO,KAAK,GAAG,GAAG,KAAK;AACpF,QAAI,SAAS,WAAW,KAAK,SAAU,YAAW;AAClD,eAAW;AACX,UAAM,KAAK,MAAM,UAAU,KAAK;AAChC,eAAW,KAAK,IAAI,UAAU,GAAG,MAAM;AACvC,eAAW,KAAK,IAAI;AAChB,YAAM,KAAK,MAAM,EAAC,MAAM,EAAE,OAAO,KAAK,KAAK,OAAO,OAAO,EAAE,MAAM,EAAE,OAAO,QAAQ,KAAK,MAAM,KAAK,MAAK,GAAG,aAAa,CAAC;AAAA,IAC5H;AAAA,EACJ;AACA,MAAI,WAAW,EAAG,QAAO,eAAe,OAAO,QAAQ,MAAM,IAAI;AACjE,SAAO,EAAC,OAAO,MAAM,UAAU,MAAM,MAAM,QAAQ,UAAU,KAAI;AACrE;AAGO,SAAS,iBAAiB,OAA0B;AACvD,QAAM,IAAI,aAAa,KAAK;AAC5B,QAAM,MAAM,CAAC,MAAa,EAAE,IAAI,OAAK,GAAG,EAAE,KAAK,IAAI,EAAE,GAAG,EAAE,EAAE,KAAK,GAAG,KAAK;AACzE,SAAO,QAAQ,IAAI,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,IAAI,CAAC;AAClD;;;AC1KO,IAAM,wBAAwB;AAAA,EACjC,oBAAoB;AACxB;AASO,IAAM,sBAA+D;AAAA,EACxE,CAAC,sBAAsB,kBAAkB,GAAG;AAAA,IACxC,sBAAsB;AAAA,IACtB,oBAAoB;AAAA,EACxB;AACJ;AAEO,IAAM,sBAAoC,sBAAsB;AAGhE,SAAS,mBAAmB,OAAqB,mBAA2B,iBAAiC;AAChH,QAAM,UAAU,oBAAoB,KAAK;AACzC,SAAO,YACH,oBAAoB,MAAY,QAAQ,uBACxC,kBAAkB,MAAY,QAAQ,oBACxC,QAAQ,CAAC,CAAC;AAChB;;;ACNO,IAAM,oBAAN,cAAgC,MAAM;AAAA;AAAA,EAElC;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EAEP,YAAY,OAAe,MAAc,QAAiB;AACtD,UAAM,GAAG,KAAK,gCAAgC,kBAAkB,IAAI,CAAC,IAAI,SAAS,KAAK,MAAM,KAAK,EAAE,EAAE;AACtG,SAAK,OAAO;AACZ,SAAK,QAAQ;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAClB;AACJ;AAGO,SAAS,oBAAoB,OAA4C;AAC5E,SAAO,CAAC,CAAC,SAAS,OAAO,UAAU,YAAa,MAA6B,SAAS;AAC1F;AAEA,IAAM,qBAAqB,CAAC,UAAU,cAAc,cAAc,aAAa,QAAQ,mBAAmB,aAAa;AAGvH,SAAS,kBAAkB,MAAsB;AAC7C,QAAM,OAAO,KAAK,MAAM,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK;AACzD,SAAO,KAAK,YAAY;AAC5B;AAEA,SAAS,YAAY,MAAuB;AACxC,QAAM,OAAO,kBAAkB,IAAI;AACnC,SAAO,mBAAmB,KAAK,YAAU,KAAK,SAAS,MAAM,CAAC;AAClE;AAUA,eAAsB,cAAc,QAAgB,QAAgB,aAA+B,OAA6B,CAAC,GAA4B;AACzJ,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,QAAe,CAAC,EAAE,MAAM,QAAQ,MAAM,OAAO,CAAC;AACpD,aAAW,OAAO,KAAK,cAAc,CAAC,GAAG;AACrC,UAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,CAAC;AAC5C,UAAM,KAAK,EAAE,MAAM,SAAS,WAAW,cAAc,MAAM,IAAI,KAAK,SAAS,QAAQ,EAAE,CAAC;AAAA,EAC5F;AACA,QAAM,UAAU,KAAK,aAAa;AAClC,QAAM,MAAM,MAAM,QAAQ,iEAAiE;AAAA,IACvF,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oBAAoB,kBAAkB,OAAO;AAAA,IACxE,MAAM,KAAK,UAAU;AAAA,MACjB;AAAA,MACA;AAAA,MACA,iBAAiB,EAAE,MAAM,SAAS,WAAW,cAAc,cAAc,aAAa,YAAY,KAAK,aAAa,KAAK;AAAA,IAC7H,CAAC;AAAA,EACL,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACT,UAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,KAAK,MAAM,IAAI,KAAK,GAAG,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,EAClG;AACA,QAAM,OAAY,MAAM,IAAI,KAAK;AACjC,QAAM,OAAO,KAAK,SAAS,CAAC,GACvB,QAAQ,CAAC,MAAW,EAAE,WAAW,CAAC,CAAC,EACnC,KAAK,CAAC,MAAW,EAAE,SAAS,WAAW,EAAE,IAAI,GAAG;AACrD,MAAI,CAAC,IAAK,OAAM,oBAAoB,OAAO,IAAI;AAE/C,QAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,QAAM,eAAe,MAAM,6BAA6B,CAAC,GACpD,OAAO,CAAC,MAAW,EAAE,aAAa,OAAO,EACzC,OAAO,CAAC,KAAa,MAAW,OAAO,EAAE,UAAU,IAAI,CAAC;AAC7D,QAAM,cAAc,MAAM,sBAAsB;AAEhD,SAAO,EAAE,QAAQ,OAAO,KAAK,KAAK,QAAQ,GAAG,SAAS,mBAAmB,OAAO,aAAa,WAAW,EAAE;AAC9G;AAGA,SAAS,oBAAoB,OAAe,MAAkB;AAC1D,QAAM,SAAkD,MAAM,QAAQ,MAAM,MAAM,IAAI,KAAK,SAAS,CAAC;AACrG,QAAM,QAAQ,OAAO,IAAI,OAAK,OAAO,GAAG,QAAQ,EAAE,CAAC,EAAE,OAAO,OAAO;AACnE,QAAM,WAAW,OAAO,IAAI,OAAM,OAAO,GAAG,YAAY,WAAW,EAAE,QAAQ,KAAK,IAAI,EAAG,EAAE,OAAO,OAAO;AACzG,QAAM,QAAQ,MAAM,KAAK,WAAW;AACpC,MAAI,OAAO;AACP,WAAO,IAAI,kBAAkB,OAAO,OAAO,SAAS,CAAC,GAAG,MAAM,GAAG,GAAG,CAAC;AAAA,EACzE;AACA,QAAM,SAAS,OAAO,MAAM,WAAW,WAAW,KAAK,SAAS;AAChE,QAAM,SAAS,CAAC,MAAM,SAAS,QAAQ,MAAM,IAAI,iBAAiB,EAAE,KAAK,IAAI,CAAC,KAAK,IAAI,SAAS,CAAC,GAAG,MAAM,GAAG,GAAG,KAAK,EAAE,EAClH,OAAO,OAAO,EACd,KAAK,IAAI;AACd,SAAO,IAAI,MAAM,kDAAkD,MAAM,GAAG,SAAS,KAAK,MAAM,KAAK,EAAE,GAAG;AAC9G;;;AC9FO,SAAS,QAAQ,OAA+C;AACnE,MAAI,SAAS,EAAG,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AAC1C,MAAI,SAAS,EAAG,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AAC1C,MAAI,SAAS,EAAG,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AAC1C,MAAI,SAAS,GAAI,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AAC3C,MAAI,SAAS,GAAI,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AAC3C,MAAI,SAAS,GAAI,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AAC3C,QAAM,IAAI,MAAM,8BAA8B,KAAK,iBAAiB;AACxE;AAEO,IAAM,wBAAwB;AAM9B,SAAS,SAAS,OAAuB,eAAuB,uBAAiG;AACpK,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ,MAAM,MAAM;AAC3C,QAAM,SAAS,CAAC,GAAG,KAAK;AACxB,WAAS,IAAI,OAAO,QAAQ,IAAI,OAAO,MAAM,KAAK;AAC9C,WAAO,KAAK,EAAE,KAAK,WAAW,CAAC,IAAI,OAAO,YAAY,QAAQ,aAAa,CAAC;AAAA,EAChF;AACA,SAAO,EAAE,OAAO,QAAQ,MAAM,MAAM,WAAW,MAAM,OAAO;AAChE;AAqBO,SAAS,yBAAyB,MAAmC;AACxE,QAAM,EAAE,OAAO,MAAM,MAAM,SAAS,SAAS,SAAS,IAAI;AAC1D,QAAM,YAAY,MAAM;AAAA,IACpB,CAAC,GAAG,MAAM,QAAQ,IAAI,CAAC,KAAK,EAAE,MAAM;AAAA,EACxC,EAAE,KAAK,IAAI;AAOX,QAAM,YAAY,WACZ,sEAAiE,QAAQ;AAAA;AAAA,IACzE;AACN,QAAM,YAAY,WACZ,kKACA;AACN,QAAM,YAAY,WAAW;AAAA;AAAA,+CAAoD,QAAQ,2DAAsD;AAE/I,SAAO,GAAG,SAAS,kCAAkC,OAAO,2DAA2D,MAAM,MAAM,uBAAuB,IAAI,gBAAgB,IAAI;AAAA;AAAA,kBAEzK,QAAQ,KAAK,MAAM,QAAQ,WAAW;AAAA;AAAA,EAEjD,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,wRAEmQ,SAAS;AACvR;AAKO,IAAM,kBAAkB;AACxB,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAqBtC,eAAsB,WAClB,OACA,KACA,OACA,OACA,MACA,MACA,OAA0B,CAAC,GAC4B;AAGvD,QAAM,OAAe,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,YAAY,iBAAiB,oBAAoB,KAAK,CAAC,EAAE,SAAS;AAC7H,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,MAAM,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,mBAAmB,KAAK,CAAC;AAC/F,QAAM,QAAQ,KAAK,OAAO,SAAS,KAAK;AACxC,MAAI,QAAQ,MAAM,QAAQ,SAAS,MAAM,KAAM,OAAM,IAAI,MAAM,0CAA0C,KAAK,IAAI,MAAM,EAAE;AAE1H,QAAM,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,MAAM,EAAE;AAC/F,QAAM,YAAY,gBAAgB,OAAO,MAAM,IAAI;AACnD,MAAI,CAAC,UAAU,UAAU;AACrB,SAAK,aAAa;AAAA,MACd,MAAM;AAAA,MACN,SAAS,iCAAiC,IAAI,IAAI,IAAI;AAAA,MACtD,QAAQ,EAAE,OAAO,QAAQ,UAAU,iBAAiB,KAAK,EAAE;AAAA,IAC/D,CAAC;AAAA,EACL,WAAW,UAAU,SAAS,QAAQ,UAAU,SAAS,MAAM;AAI3D,SAAK,aAAa;AAAA,MACd,MAAM;AAAA,MACN,SAAS,kBAAkB,UAAU,IAAI,IAAI,UAAU,IAAI,eAAe,IAAI,IAAI,IAAI;AAAA,MACtF,QAAQ,EAAE,OAAO,QAAQ,YAAY,UAAU,MAAM,QAAQ,QAAQ,MAAM;AAAA,IAC/E,CAAC;AAAA,EACL;AACA,QAAM,WAAW,eAAe,OAAO,QAAQ,MAAM,IAAI,EAAE;AAE3D,QAAM,SAA0B,CAAC;AACjC,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC5B,UAAM,UAAU,eAAe,UAAU,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC;AAChE,UAAM,OAAO,MAAM,QAAQ,OAAO,MAAM,QAAQ,IAAI;AACpD,WAAO,KAAK,EAAE,KAAK,MAAM,CAAC,EAAE,KAAK,OAAO,MAAM,CAAC,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,EAC3E;AAEA,MAAI,YAAoB,MAAM,MAAM,IAAI,EAAE,KAAK,EAAE,SAAS,oBAAoB,SAAS,KAAK,CAAC,EAAE,SAAS;AACxG,MAAI,UAAU,SAAS,IAAI,IAAI,wBAAwB;AACnD,gBAAY,MAAM,MAAM,IAAI,EAAE,KAAK,EAAE,SAAS,IAAI,SAAS,KAAK,CAAC,EAAE,SAAS;AAAA,EAChF;AACA,SAAO;AAAA,IACH;AAAA,IACA,OAAO,EAAE,MAAM,WAAW,OAAO,QAAQ,OAAO,UAAU,MAAM,MAAM,GAAG,KAAK,GAAG,UAAU,UAAU,SAAS;AAAA,EAClH;AACJ;AAGA,eAAsB,QAAQ,OAAkB,OAAe,MAAkC;AAC7F,SAAO,MAAM,KAAK,EACb,QAAQ,IAAI,EACZ,OAAO,eAAe,cAAc,EACpC,KAAK,EAAE,SAAS,GAAG,CAAC,EACpB,SAAS;AAClB;AAuBA,eAAsB,kBAAkB,QAAgB,OAAkB,MAA6B,OAAiC,CAAC,GAAgC;AACrK,QAAM,EAAE,OAAO,MAAM,MAAM,UAAU,IAAI,SAAS,KAAK,OAAO,KAAK,YAAY;AAC/E,QAAM,SAAS,yBAAyB,EAAE,OAAO,MAAM,MAAM,SAAS,KAAK,SAAS,SAAS,KAAK,SAAS,UAAU,KAAK,SAAS,CAAC;AACpI,QAAM,QAAQ,MAAM,cAAc,QAAQ,QAAQ,OAAO,KAAK,KAAK;AACnE,QAAM,EAAE,QAAQ,MAAM,IAAI,MAAM,WAAW,OAAO,MAAM,QAAQ,OAAO,WAAW,MAAM,MAAM,IAAI;AAClG,SAAO,EAAE,WAAW,QAAQ,OAAO,SAAS,MAAM,QAAQ;AAC9D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/images/index.ts","../src/images/types.ts","../src/images/framing.ts","../src/images/sheet-detection.ts","../src/images/image-catalog.ts","../src/images/google-image.ts","../src/images/portrait-sheet.ts"],"sourcesContent":["/**\n * `@hiper2d/ai-agents/images` — image generation and portrait sheets.\n *\n * Kept off the main entry on purpose: this subpath is for hosts that draw pictures.\n * It never imports sharp (the host passes its own instance in), so nothing native\n * rides along, and the geometry helpers are pure and safe in a browser bundle.\n */\nexport type {\n ImageRect, ImageSize, AvatarCircle, AvatarFraming, ImageFocus,\n PortraitCell, PortraitSlice, DrawnSheet, GeneratedImage, SharpLike, SharpPipeline,\n} from './types';\nexport {\n CARD_ASPECT, CARD_WIDTH_PX, CARD_HEIGHT_PX, DEFAULT_AVATAR_CIRCLE, MIN_CARD_HEIGHT_FRACTION,\n} from './types';\n\nexport {\n cardInCell, defaultFraming, fitCircle, fitCard, fitFraming, isFramingShape,\n circleFocus, cardFocus, circleFocusOnSheet, focusToBackground,\n} from './framing';\n\nexport { findDividers, equalSplitGrid, detectSheetGrid, describeDividers } from './sheet-detection';\nexport type { GreyPlane, SheetGrid } from './sheet-detection';\n\nexport {\n IMAGE_MODEL_CONSTANTS, IMAGE_MODEL_PRICING, DEFAULT_IMAGE_MODEL, calculateImageCost,\n} from './image-catalog';\nexport type { ImageModelId, ImageModelPricing } from './image-catalog';\n\nexport { generateImage, ImageRefusalError, isImageRefusalError } from './google-image';\nexport type { GenerateImageOptions, ImageAspectRatio } from './google-image';\n\nexport {\n gridFor, padCells, buildPortraitSheetPrompt, sliceSheet, cutCard, drawPortraitSheet,\n DEFAULT_FILLER_PROMPT, SHEET_MAX_WIDTH, SHEET_JPEG_QUALITY, SHEET_MAX_BASE64_BYTES,\n} from './portrait-sheet';\nexport type {\n PortraitSheetPrompt, SheetMismatch, SliceSheetOptions,\n DrawPortraitSheetSpec, DrawPortraitSheetOptions, DrawnPortraitSheet,\n} from './portrait-sheet';\n","/**\n * Shared shapes for the portrait-sheet pipeline. Coordinates are SHEET pixels unless\n * stated otherwise; a card is portrait 3:4 and the circle inside it is card-relative.\n */\n\n/** A rectangle in image pixels (sharp's `extract` shape). */\nexport interface ImageRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport interface ImageSize {\n width: number;\n height: number;\n}\n\n/** The round avatar inside a card. `x` and `d` are fractions of the card's width,\n * `y` a fraction of its height. */\nexport interface AvatarCircle {\n x: number;\n y: number;\n d: number;\n}\n\n/** Where a portrait comes from on its sheet: the card cut and the circle in it. */\nexport interface AvatarFraming {\n card: ImageRect;\n circle: AvatarCircle;\n}\n\n/** The portion of an image to display, as fractions of its width/height. */\nexport interface ImageFocus {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n// Cards are portrait 3:4 (a poster's shape), stored at this size.\nexport const CARD_ASPECT = 3 / 4; // width / height\nexport const CARD_WIDTH_PX = 600;\nexport const CARD_HEIGHT_PX = 800;\n// Where the circle starts on a freshly cut card: 86% of its width, centred,\n// near the top — wide enough to take in a chin or beard (0.72 cut them off).\nexport const DEFAULT_AVATAR_CIRCLE: AvatarCircle = { x: 0.07, y: 0.02, d: 0.86 };\n// A card narrower than this fraction of the sheet's height upscales too much.\nexport const MIN_CARD_HEIGHT_FRACTION = 0.12;\n\n/** One character on a portrait sheet. */\nexport interface PortraitCell {\n /** Stable id the caller stores the crop under; never drawn into the image. */\n key: string;\n /** Character name, for logs and prompt guidance; never drawn into the image. */\n label: string;\n /** One-line visual description for this cell (gender, name, appearance). */\n prompt: string;\n}\n\n/** A card cut from a drawn sheet. */\nexport interface PortraitSlice {\n key: string;\n label: string;\n jpeg: Buffer;\n /** The framing the slicer chose: the card on the sheet and the default circle. */\n framing: AvatarFraming;\n}\n\n/** The kept sheet: the grid image itself (re-encoded), its size and the cells\n * the cards were cut from — enough to re-cut any card later at a new framing. */\nexport interface DrawnSheet {\n jpeg: Buffer;\n width: number;\n height: number;\n cells: ImageRect[];\n /** false = no divider lines were found and the cells are the equal split of the\n * requested grid; true = the cells between the lines the model actually drew. */\n detected: boolean;\n}\n\nexport interface GeneratedImage {\n buffer: Buffer;\n costUSD: number;\n}\n\n/**\n * The slice of sharp's API the pipeline uses. The library never imports sharp — a\n * native module the host installs and passes in (`(await import('sharp')).default`) —\n * so text-only consumers pull in nothing new.\n */\nexport type SharpLike = (input: Buffer) => {\n resize(options: { width: number; withoutEnlargement?: boolean }): SharpPipeline;\n resize(width: number, height: number): SharpPipeline;\n greyscale(): SharpPipeline;\n extract(rect: ImageRect): SharpPipeline;\n jpeg(options: { quality: number; mozjpeg?: boolean }): SharpPipeline;\n raw(): SharpPipeline;\n toBuffer(): Promise<Buffer>;\n toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: { width: number; height: number } }>;\n metadata(): Promise<{ width?: number; height?: number }>;\n};\n\nexport type SharpPipeline = ReturnType<SharpLike>;\n","import {\n AvatarCircle,\n AvatarFraming,\n CARD_ASPECT,\n DEFAULT_AVATAR_CIRCLE,\n ImageFocus,\n ImageRect,\n ImageSize,\n MIN_CARD_HEIGHT_FRACTION,\n} from './types';\n\n/**\n * Pure framing geometry, shared by the slicer (server), reframe validation (server)\n * and renderers/editors (browser). No I/O here.\n *\n * Coordinates: a card is an ImageRect in SHEET pixels (portrait 3:4); the circle is\n * card-relative (x, d = fractions of the card's width, y = fraction of its height).\n * An ImageFocus is what a renderer needs: the fraction of an image to show, so the\n * same avatar component can show a circle cut out of a stored card OR out of a\n * static preset sheet.\n */\n\nconst clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));\n\n/** Largest 3:4 card that fits a cell, centered horizontally and top-aligned:\n * portraits are busts with the head near the top, so the top edge is the\n * anchor that keeps the face in. */\nexport function cardInCell(cell: ImageRect): ImageRect {\n const width = Math.floor(Math.min(cell.width, cell.height * CARD_ASPECT));\n const height = Math.min(cell.height, Math.round(width / CARD_ASPECT));\n return {\n left: Math.round(cell.left + (cell.width - width) / 2),\n top: cell.top,\n width,\n height,\n };\n}\n\nexport function defaultFraming(cell: ImageRect): AvatarFraming {\n return { card: cardInCell(cell), circle: { ...DEFAULT_AVATAR_CIRCLE } };\n}\n\n/** Keeps the circle inside its card. */\nexport function fitCircle(circle: AvatarCircle): AvatarCircle {\n const d = clamp(circle.d, 0.15, 1);\n return {\n d,\n x: clamp(circle.x, 0, 1 - d),\n y: clamp(circle.y, 0, 1 - d * CARD_ASPECT),\n };\n}\n\n/** Snaps a card to a valid 3:4 rectangle inside the sheet, at least the\n * minimum size. Width is the free variable; height follows the aspect. */\nexport function fitCard(card: ImageRect, sheet: ImageSize): ImageRect {\n const minHeight = Math.max(16, Math.round(sheet.height * MIN_CARD_HEIGHT_FRACTION));\n const maxHeight = Math.min(sheet.height, sheet.width / CARD_ASPECT);\n const height = Math.round(clamp(card.height, minHeight, maxHeight));\n const width = Math.round(height * CARD_ASPECT);\n return {\n width,\n height,\n left: Math.round(clamp(card.left, 0, sheet.width - width)),\n top: Math.round(clamp(card.top, 0, sheet.height - height)),\n };\n}\n\nexport function fitFraming(framing: AvatarFraming, sheet: ImageSize): AvatarFraming {\n return { card: fitCard(framing.card, sheet), circle: fitCircle(framing.circle) };\n}\n\n/** True when the framing is a well-formed object with finite numbers. Shape\n * only — call fitFraming for the geometric bounds. */\nexport function isFramingShape(value: unknown): value is AvatarFraming {\n if (!value || typeof value !== 'object') return false;\n const f = value as any;\n const rect = f.card, circle = f.circle;\n const num = (v: unknown) => typeof v === 'number' && Number.isFinite(v);\n return !!rect && !!circle\n && num(rect.left) && num(rect.top) && num(rect.width) && num(rect.height)\n && rect.width > 0 && rect.height > 0\n && num(circle.x) && num(circle.y) && num(circle.d) && circle.d > 0;\n}\n\n/** The circle as a focus on the CARD image itself (a stored card cut). */\nexport function circleFocus(circle: AvatarCircle): ImageFocus {\n const c = fitCircle(circle);\n return { x: c.x, y: c.y, w: c.d, h: c.d * CARD_ASPECT };\n}\n\n/** The card as a focus on its sheet. */\nexport function cardFocus(card: ImageRect, sheet: ImageSize): ImageFocus {\n return {\n x: card.left / sheet.width,\n y: card.top / sheet.height,\n w: card.width / sheet.width,\n h: card.height / sheet.height,\n };\n}\n\n/** The circle as a focus on the SHEET (card and circle composed) — how a preset\n * mannequin renders straight off a static sheet. */\nexport function circleFocusOnSheet(framing: AvatarFraming, sheet: ImageSize): ImageFocus {\n const c = fitCircle(framing.circle);\n const card = framing.card;\n const side = c.d * card.width; // the circle is square, in sheet px\n return {\n x: (card.left + c.x * card.width) / sheet.width,\n y: (card.top + c.y * card.height) / sheet.height,\n w: side / sheet.width,\n h: side / sheet.height,\n };\n}\n\n/** CSS `background-size` / `background-position` that shows exactly `focus`\n * of an image inside a box. */\nexport function focusToBackground(focus: ImageFocus): { backgroundSize: string; backgroundPosition: string } {\n const axis = (pos: number, ext: number) => ext >= 1 ? 50 : (pos / (1 - ext)) * 100;\n return {\n backgroundSize: `${(100 / focus.w).toFixed(3)}% ${(100 / focus.h).toFixed(3)}%`,\n backgroundPosition: `${axis(focus.x, focus.w).toFixed(3)}% ${axis(focus.y, focus.h).toFixed(3)}%`,\n };\n}\n","import { ImageRect } from \"./types\";\n\n/**\n * Finds the cells of a drawn portrait sheet from its pixels.\n *\n * The prompt asks for equal cells separated by thin dark divider lines, but\n * the model draws the lines where it likes: on a correctly drawn 4x4 sheet\n * the rows measured 497 / 485 / 428 / 382 px where equal splits assumed 448\n * (2026-09-02), so fixed-interval slicing cut lower rows through the previous\n * character's chest. The lines themselves are crisp — near-black across the\n * full width — so they are easy to find, and the cells between them are the\n * real ones. Columns have always measured exact; rows drift.\n *\n * Pure over a greyscale byte plane so it is testable without sharp.\n */\n\nexport interface GreyPlane {\n width: number;\n height: number;\n // width*height bytes, row-major, 0 = black.\n data: Uint8Array;\n}\n\nexport interface SheetGrid {\n cells: ImageRect[]; // row-major\n cols: number;\n rows: number;\n // false = no usable divider lines were found and this is the equal split\n // of the REQUESTED grid. true = the cells between the lines the model\n // actually drew, which may be a different grid than requested (a 4x4\n // request has come back as 4x3 and as 6x3) — the sheet is the truth.\n detected: boolean;\n}\n\n// A pixel darker than this is \"line\"; a scanline with more than this share\n// of dark pixels is a divider. Cell backgrounds are flat muted colours, well\n// above the threshold, and the dark portrait clothing never spans a full line.\nconst DARK_MAX = 60;\nconst DIVIDER_SHARE = 0.85;\n// Pixels kept clear of a divider when cutting (anti-aliased edges).\nconst DIVIDER_INSET = 4;\n// A real divider is a thin line (~7 px on a 2400 px sheet). Thinner runs are\n// specks (a dark hairline inside a hooded figure once passed as a row line).\n// Thicker runs up to a small share of the sheet are still boundaries: the\n// model sometimes adds a dark name plate under each portrait despite the\n// no-text rule, and the plate belongs OUTSIDE the cell. Beyond that share a\n// dark run is a dark art style's background, not a boundary.\nconst DIVIDER_MIN_THICKNESS = 3;\nconst DIVIDER_MAX_THICKNESS_SHARE = 0.08;\n// A span between lines narrower than this share of the sheet can't hold a\n// face; the line that would create it is not a cell boundary.\nconst MIN_SPAN_SHARE = 0.12;\n\ninterface Run { start: number; end: number }\n\nfunction runs(indices: number[]): Run[] {\n const out: Run[] = [];\n for (const i of indices) {\n const last = out[out.length - 1];\n if (last && i === last.end + 1) last.end = i;\n else out.push({start: i, end: i});\n }\n return out;\n}\n\n/** Dark runs along one axis, measured over a window of the other axis (a\n * band of rows for columns, the full height by default). Border runs and\n * runs that aren't line-shaped are dropped. */\nfunction lineRuns(plane: GreyPlane, axis: 'rows' | 'cols', from: number, to: number): Run[] {\n const {width, height, data} = plane;\n const extent = axis === 'rows' ? height : width;\n const dark: number[] = [];\n for (let i = 0; i < extent; i++) {\n let count = 0;\n if (axis === 'rows') {\n const base = i * width;\n for (let x = from; x < to; x++) if (data[base + x] < DARK_MAX) count++;\n } else {\n for (let y = from; y < to; y++) if (data[y * width + i] < DARK_MAX) count++;\n }\n if (count / (to - from) > DIVIDER_SHARE) dark.push(i);\n }\n const maxThickness = extent * DIVIDER_MAX_THICKNESS_SHARE;\n return runs(dark)\n .filter(r => r.start > 2 && r.end < extent - 3)\n .filter(r => { const t = r.end - r.start + 1; return t >= DIVIDER_MIN_THICKNESS && t <= maxThickness; });\n}\n\n/** Lines that would leave a sliver on either side are not boundaries. */\nfunction dropSliverMakers(lines: Run[], extent: number): Run[] {\n const minSpan = extent * MIN_SPAN_SHARE;\n const kept: Run[] = [];\n let cursor = 0;\n for (const line of lines) {\n if (line.start - cursor < minSpan || extent - (line.end + 1) < minSpan) continue;\n kept.push(line);\n cursor = line.end + 1;\n }\n return kept;\n}\n\n/** Divider runs across the whole plane: row lines over the full width,\n * column lines over the full height. */\nexport function findDividers(plane: GreyPlane): {rows: Run[]; cols: Run[]} {\n return {\n rows: dropSliverMakers(lineRuns(plane, 'rows', 0, plane.width), plane.height),\n cols: dropSliverMakers(lineRuns(plane, 'cols', 0, plane.height), plane.width),\n };\n}\n\n/** Cells between dividers along one axis, from the border to the first line,\n * line to line, last line to the border. */\nfunction spans(dividers: Run[], extent: number): {start: number; end: number}[] {\n const out: {start: number; end: number}[] = [];\n let cursor = 0;\n for (const d of dividers) {\n out.push({start: cursor, end: d.start});\n cursor = d.end + 1;\n }\n out.push({start: cursor, end: extent});\n return out;\n}\n\nexport function equalSplitGrid(width: number, height: number, cols: number, rows: number): SheetGrid {\n const cellW = Math.floor(width / cols), cellH = Math.floor(height / rows);\n const cells: ImageRect[] = [];\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n cells.push(inset({left: c * cellW, top: r * cellH, width: cellW, height: cellH}, 8));\n }\n }\n return {cells, cols, rows, detected: false};\n}\n\nfunction inset(rect: ImageRect, by: number): ImageRect {\n return {left: rect.left + by, top: rect.top + by, width: Math.max(1, rect.width - 2 * by), height: Math.max(1, rect.height - 2 * by)};\n}\n\n/**\n * The sheet's cells. Read off the divider lines the model drew whenever they\n * form a plausible grid, even when that grid differs from the cols x rows\n * that were asked for — the model keeps row-major order, so the drawn cells\n * still map to the characters in sequence. Row lines are found over the\n * full width; column lines are found PER ROW BAND, because the model has\n * drawn rows with different column layouts (and a column line that stops\n * short of the last row). A band without lines of its own borrows the\n * layout of the band above it. The equal split of the requested grid is the\n * last resort when no lines are found at all (the caller logs it; the kept\n * sheet lets the owner reframe by hand either way).\n */\nexport function detectSheetGrid(plane: GreyPlane, cols: number, rows: number): SheetGrid {\n const {width, height} = plane;\n const rowLines = dropSliverMakers(lineRuns(plane, 'rows', 0, width), height);\n const bands = spans(rowLines, height);\n const cells: ImageRect[] = [];\n let previous: Run[] | null = null;\n let colCount = 0;\n for (const band of bands) {\n let colLines = dropSliverMakers(lineRuns(plane, 'cols', band.start, band.end), width);\n if (colLines.length === 0 && previous) colLines = previous;\n previous = colLines;\n const xs = spans(colLines, width);\n colCount = Math.max(colCount, xs.length);\n for (const x of xs) {\n cells.push(inset({left: x.start, top: band.start, width: x.end - x.start, height: band.end - band.start}, DIVIDER_INSET));\n }\n }\n if (colCount < 2) return equalSplitGrid(width, height, cols, rows);\n return {cells, cols: colCount, rows: bands.length, detected: true};\n}\n\n/** What findDividers saw, for the mismatch log line. */\nexport function describeDividers(plane: GreyPlane): string {\n const d = findDividers(plane);\n const fmt = (r: Run[]) => r.map(x => `${x.start}-${x.end}`).join(',') || 'none';\n return `rows@${fmt(d.rows)} cols@${fmt(d.cols)}`;\n}\n","/**\n * Image-generation models: platform-side, never user-selected. Prices per 1M tokens\n * from ai.google.dev/gemini-api/docs/pricing (2026-08). Image output bills ~1120 tokens\n * per image regardless of resolution, so one image ≈ $0.067 — fewer calls, not lower\n * resolution, is what minimizes cost.\n */\nexport const IMAGE_MODEL_CONSTANTS = {\n GEMINI_FLASH_IMAGE: 'gemini-3.1-flash-image',\n} as const;\n\nexport type ImageModelId = typeof IMAGE_MODEL_CONSTANTS[keyof typeof IMAGE_MODEL_CONSTANTS];\n\nexport interface ImageModelPricing {\n imageOutputPricePerM: number;\n textInputPricePerM: number;\n}\n\nexport const IMAGE_MODEL_PRICING: Record<ImageModelId, ImageModelPricing> = {\n [IMAGE_MODEL_CONSTANTS.GEMINI_FLASH_IMAGE]: {\n imageOutputPricePerM: 60,\n textInputPricePerM: 0.5,\n },\n};\n\nexport const DEFAULT_IMAGE_MODEL: ImageModelId = IMAGE_MODEL_CONSTANTS.GEMINI_FLASH_IMAGE;\n\n/** Cost of one image call from the usage Gemini reports. */\nexport function calculateImageCost(model: ImageModelId, imageOutputTokens: number, textInputTokens: number): number {\n const pricing = IMAGE_MODEL_PRICING[model];\n return parseFloat((\n imageOutputTokens / 1_000_000 * pricing.imageOutputPricePerM +\n textInputTokens / 1_000_000 * pricing.textInputPricePerM\n ).toFixed(6));\n}\n","import { calculateImageCost, DEFAULT_IMAGE_MODEL, ImageModelId } from './image-catalog';\nimport { GeneratedImage } from './types';\n\nexport type ImageAspectRatio = '1:1' | '3:4' | '4:3' | '3:2' | '2:3' | '16:9' | '9:16';\n\nexport interface GenerateImageOptions {\n /** Labeled reference JPEGs (an established scene, character portraits) that\n * anchor the drawing to a known style, place or faces. */\n references?: { label: string; jpeg: Buffer }[];\n imageSize?: '1K' | '2K';\n model?: ImageModelId;\n /** Test seam / custom transport; defaults to the global fetch. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Gemini's Interactions API blocks a drawing in two shapes (both seen in production,\n * 2026-10-13):\n *\n * - **HTTP 400** with `{\"error\": {\"message\": \"Image generation blocked due to safety\n * violations. Please modify your input and retry.\", \"code\": \"Unable to show the generated\n * image. The image was filtered out because it violated Google's Generative AI Prohibited\n * Use policy …\"}}` — or \"Request blocked due to prohibited content guidelines …\". This is\n * what an explicit prompt gets.\n * - **HTTP 200** with no image block, `status: \"failed\" | \"incomplete\"` and an `errors[]`\n * entry whose `code` names the block (https://ai.google.dev/gemini-api/docs/api-errors,\n * \"Generation blocked\"): `image_safety`, `image_prohibited_content`, `image_recitation`,\n * `image_other`, plus the text-side `safety`, `prohibited_content`, `recitation`,\n * `blocklist`, `spii`, `content_blocked`. `no_image` is a plain generation failure.\n *\n * Deliberately NOT a `ModelRefusalError` subclass: this subpath is bundled apart from the\n * main entry, so an `instanceof` against the main entry's class would never match. Hosts\n * check `isImageRefusalError(error)` (by name) instead. Not retryable on the same prompt.\n */\nexport class ImageRefusalError extends Error {\n /** The block code as Gemini sent it: an `errors[].code` (a URI or bare `snake_case`\n * name) on the HTTP 200 shape, or `http_400` on the error-body shape. */\n public code: string;\n /** Gemini's human-readable message, when it sent one. */\n public reason?: string;\n public model: string;\n\n constructor(model: string, code: string, reason?: string) {\n super(`${model} declined to draw the image (${describeBlockCode(code)})${reason ? `: ${reason}` : ''}`);\n this.name = 'ImageRefusalError';\n this.model = model;\n this.code = code;\n this.reason = reason;\n }\n}\n\n/** Works across bundle copies and serialization boundaries (checks the name, not the class). */\nexport function isImageRefusalError(error: unknown): error is ImageRefusalError {\n return !!error && typeof error === 'object' && (error as { name?: unknown }).name === 'ImageRefusalError';\n}\n\nconst BLOCK_CODE_MARKERS = ['safety', 'prohibited', 'recitation', 'blocklist', 'spii', 'content_blocked', 'image_other'];\n\n/** Wording Gemini uses in a 400 body or a text block when it filtered the drawing. */\nconst BLOCK_TEXT = /blocked|safety violation|prohibited|filtered out|prohibited use policy/i;\n\n/** The trailing identifier of a code URI such as `https://…/api-errors#image_safety`. */\nfunction describeBlockCode(code: string): string {\n const tail = code.split(/[#/]/).filter(Boolean).pop() ?? code;\n return tail.toLowerCase();\n}\n\nfunction isBlockCode(code: string): boolean {\n const tail = describeBlockCode(code);\n return BLOCK_CODE_MARKERS.some(marker => tail.includes(marker));\n}\n\n/** Gemini's messages end in a markdown link to the policy; keep the sentence, drop the link. */\nfunction trimReason(text: string): string {\n return text.replace(/\\s*\\[[^\\]]*\\]\\([^)]*\\)[^.]*\\.?/g, '').trim().slice(0, 300);\n}\n\n/**\n * One image-model call on Gemini's Interactions API. Pure — no auth, no billing —\n * and the result reports its cost so the host decides whom to charge. Cost is per\n * IMAGE, not per pixel (a 1K and a 2K image both bill ~1120 output tokens).\n *\n * Throws `ImageRefusalError` when Gemini blocked the drawing (see the class), and a\n * plain `Error` naming the HTTP status, or the interaction status and error code, for\n * anything else.\n */\nexport async function generateImage(apiKey: string, prompt: string, aspectRatio: ImageAspectRatio, opts: GenerateImageOptions = {}): Promise<GeneratedImage> {\n const model = opts.model ?? DEFAULT_IMAGE_MODEL;\n const input: any[] = [{ type: 'text', text: prompt }];\n for (const ref of opts.references ?? []) {\n input.push({ type: 'text', text: ref.label });\n input.push({ type: 'image', mime_type: 'image/jpeg', data: ref.jpeg.toString('base64') });\n }\n const doFetch = opts.fetchImpl ?? fetch;\n const res = await doFetch('https://generativelanguage.googleapis.com/v1beta/interactions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },\n body: JSON.stringify({\n model,\n input,\n response_format: { type: 'image', mime_type: 'image/jpeg', aspect_ratio: aspectRatio, image_size: opts.imageSize ?? '2K' },\n }),\n });\n if (!res.ok) {\n throw httpError(model, res.status, await res.text());\n }\n const json: any = await res.json();\n const b64 = (json.steps || [])\n .flatMap((s: any) => s.content || [])\n .find((c: any) => c.type === 'image' && c.data)?.data;\n if (!b64) throw imagelessReplyError(model, json);\n\n const usage = json.usage || {};\n const imageTokens = (usage.output_tokens_by_modality || [])\n .filter((m: any) => m.modality === 'image')\n .reduce((sum: number, m: any) => sum + (m.tokens || 0), 0);\n const inputTokens = usage.total_input_tokens || 0;\n\n return { buffer: Buffer.from(b64, 'base64'), costUSD: calculateImageCost(model, imageTokens, inputTokens) };\n}\n\n/** Classifies a non-2xx reply: a 400 whose body says the drawing was filtered is a refusal. */\nfunction httpError(model: string, status: number, body: string): Error {\n if (status === 400) {\n let error: { message?: unknown; code?: unknown } | undefined;\n try { error = JSON.parse(body)?.error; } catch { /* not JSON */ }\n const message = typeof error?.message === 'string' ? error.message : '';\n const code = typeof error?.code === 'string' ? error.code : '';\n if (BLOCK_TEXT.test(message) || BLOCK_TEXT.test(code)) {\n return new ImageRefusalError(model, 'http_400', trimReason(message || code));\n }\n }\n return new Error(`Image request failed: HTTP ${status} ${body.slice(0, 600)}`);\n}\n\n/** Classifies an HTTP 200 reply that carried no image block. */\nfunction imagelessReplyError(model: string, json: any): Error {\n const errors: { code?: unknown; message?: unknown }[] = Array.isArray(json?.errors) ? json.errors : [];\n const codes = errors.map(e => String(e?.code ?? '')).filter(Boolean);\n const messages = errors.map(e => (typeof e?.message === 'string' ? e.message.trim() : '')).filter(Boolean);\n const block = codes.find(isBlockCode);\n if (block) {\n return new ImageRefusalError(model, block, messages[0] ? trimReason(messages[0]) : undefined);\n }\n // A reply with no errors[] sometimes explains itself in a text block instead.\n const text: string = (json?.steps || [])\n .flatMap((s: any) => s.content || [])\n .filter((c: any) => c.type === 'text' && typeof c.text === 'string')\n .map((c: any) => c.text.trim())\n .join(' ')\n .slice(0, 300);\n if (!codes.length && BLOCK_TEXT.test(text)) {\n return new ImageRefusalError(model, 'text_block', trimReason(text));\n }\n const status = typeof json?.status === 'string' ? json.status : 'unknown';\n const detail = [\n codes.length ? `code ${codes.map(describeBlockCode).join(', ')}` : '',\n messages[0]?.slice(0, 300) ?? '',\n text ? `text \"${text}\"` : '',\n ].filter(Boolean).join('; ');\n return new Error(`Image response contained no image data (status ${status}${detail ? `; ${detail}` : ''})`);\n}\n","import { generateImage, GenerateImageOptions } from './google-image';\nimport { defaultFraming } from './framing';\nimport { describeDividers, detectSheetGrid, equalSplitGrid } from './sheet-detection';\nimport { CARD_HEIGHT_PX, CARD_WIDTH_PX, DrawnSheet, ImageRect, PortraitCell, PortraitSlice, SharpLike } from './types';\n\n/**\n * Portrait sheets: one image-model call draws a whole cast as a grid of bust\n * portraits, and the grid is cut into one card per character. One call costs the\n * same whether it draws one face or sixteen, so this is the cheap way to give\n * every character in a game a portrait in a single consistent style.\n *\n * Nothing here touches storage or billing: the host stores the cards and the kept\n * sheet (so a card can be re-cut later at a new framing) and charges whoever it\n * likes for `costUSD`. sharp is passed in, never imported.\n */\n\n/**\n * Grid dimensions by cell count on the 4:3 canvas. Cells must come out\n * square-to-portrait: asked for 4x4 (landscape 600x448 cells) the model redrew the\n * sheet as 4x3, 6x3 and once as an irregular two-layout sheet — every time towards\n * taller cells. 5x3 keeps 480x597 cells for 13-15; 17 cells get 6x3 (400x600, 2:3,\n * one of the shapes the model volunteered). Never 4x4: it silently cut a 17-cell\n * cast to 16.\n */\nexport function gridFor(cells: number): { cols: number; rows: number } {\n if (cells <= 6) return { cols: 3, rows: 2 };\n if (cells <= 8) return { cols: 4, rows: 2 };\n if (cells <= 9) return { cols: 3, rows: 3 };\n if (cells <= 12) return { cols: 4, rows: 3 };\n if (cells <= 15) return { cols: 5, rows: 3 };\n if (cells <= 18) return { cols: 6, rows: 3 };\n throw new Error(`Portrait sheet cannot hold ${cells} cells (max 18)`);\n}\n\nexport const DEFAULT_FILLER_PROMPT = `\"Stranger\" — an anonymous hooded figure fitting the setting, face hidden in shadow`;\n\n/**\n * Pads a cast out to a full grid: the model draws exactly cols x rows cells, so the\n * spare ones get an anonymous filler that is drawn and discarded.\n */\nexport function padCells(cells: PortraitCell[], fillerPrompt: string = DEFAULT_FILLER_PROMPT): { cells: PortraitCell[]; cols: number; rows: number; realCount: number } {\n const { cols, rows } = gridFor(cells.length);\n const padded = [...cells];\n for (let i = padded.length; i < cols * rows; i++) {\n padded.push({ key: `__filler${i}`, label: 'Stranger', prompt: fillerPrompt });\n }\n return { cells: padded, cols, rows, realCount: cells.length };\n}\n\nexport interface PortraitSheetPrompt {\n cells: PortraitCell[];\n cols: number;\n rows: number;\n /** What the sheet is for, e.g. \"a social deduction game\" — one noun phrase. */\n purpose: string;\n /** The world the characters live in: a title and a short description. */\n setting: { title: string; description: string };\n /** Player-chosen art direction, already sanitized by the host. Replaces the\n * model's free choice of style. */\n artStyle?: string;\n}\n\n/**\n * The sheet prompt. The wording is load-bearing: equal cells with thin dark\n * divider lines are what the slicer finds, one flat muted background per cell keeps\n * the lines detectable in any art style, and the no-text rule is repeated because\n * the model otherwise typesets names and bios into the image.\n */\nexport function buildPortraitSheetPrompt(spec: PortraitSheetPrompt): string {\n const { cells, cols, rows, purpose, setting, artStyle } = spec;\n const cellLines = cells.map(\n (c, i) => `Cell ${i + 1}: ${c.prompt}. Its own distinct flat solid muted background color.`\n ).join('\\n');\n\n // A player style leads the prompt and is restated at the end: the sheet\n // framing (grid, bust portraits, flat backgrounds) otherwise pulls the model\n // toward drawn character-sheet art whatever style was asked for (an\n // \"ultra-realism\" game came out as comic portraits). The grid wording stays\n // intact — it is what the slicer relies on — but is declared layout only.\n const styleLead = artStyle\n ? `Art style, chosen by the player — it governs every portrait: \"${artStyle}\". Each cell must look made in that style: a photographic or cinematic style means real photographs, a painted style means paintings.\\n\\n`\n : '';\n const styleLine = artStyle\n ? `Render every portrait in the player's art style above. Apply it consistently to every portrait: same rendering technique, same palette family, same lighting.`\n : `Choose ONE cohesive illustration style that fits this setting and apply it consistently to every portrait: same rendering technique, same palette family, same lighting.`;\n const styleTail = artStyle ? `\\n\\nEvery cell must be fully in this art style: \"${artStyle}\" — and still carry no names or text of any kind.` : '';\n\n return `${styleLead}A character portrait sheet for ${purpose}, rendered as a single image: a precise grid of exactly ${cells.length} rectangular cells, ${cols} columns and ${rows} rows, all cells exactly equal size, separated by thin dark divider lines. Each cell contains one bust portrait (head and shoulders) of a different character, centered in its cell.\n\nSetting — \"${setting.title}\": ${setting.description}\n\n${styleLine} Every face must be distinct and memorable, and match its character description. No character may span more than one cell. Give each cell its own flat solid muted desaturated background color, different from its neighbors. Row-major order, left to right, top to bottom:\n\n${cellLines}\n\nThe character descriptions above are guidance for the drawing only — NEVER render them as text. Absolutely no text anywhere in the image: no names, no labels, no captions, no letters, no writing of any kind — and no lettering on clothing, equipment, insignia or logos.${styleTail}`;\n}\n\n// The model returns ~2.4 MB loosely-compressed JPEGs; at this width and quality a\n// sheet is ~400 KB (~530 KB as base64), under a 1 MiB document limit with cells\n// still ~500 px tall.\nexport const SHEET_MAX_WIDTH = 2400;\nexport const SHEET_JPEG_QUALITY = 85;\nexport const SHEET_MAX_BASE64_BYTES = 900_000;\n\n/** Why a sheet's cells did not come out as requested; the host decides how loudly to log. */\nexport interface SheetMismatch {\n kind: 'no-dividers' | 'different-grid';\n message: string;\n detail: Record<string, unknown>;\n}\n\nexport interface SliceSheetOptions {\n onMismatch?: (mismatch: SheetMismatch) => void;\n /** Long edge the sheet is normalized to before cutting; defaults to SHEET_MAX_WIDTH. */\n maxWidth?: number;\n}\n\n/**\n * Cuts the cards out of a drawn sheet and keeps the sheet. Cells come from the\n * divider lines the model drew (equal split as a last resort); each card is the\n * largest 3:4 rectangle in its cell, top-anchored, with the default circle — the\n * framing the host can let a user move later.\n */\nexport async function sliceSheet(\n sharp: SharpLike,\n raw: Buffer,\n cells: PortraitCell[],\n count: number,\n cols: number,\n rows: number,\n opts: SliceSheetOptions = {},\n): Promise<{ slices: PortraitSlice[]; sheet: DrawnSheet }> {\n // Normalise the working resolution first so cells, cards and the stored sheet\n // all share one pixel space.\n const grid: Buffer = await sharp(raw).resize({ width: opts.maxWidth ?? SHEET_MAX_WIDTH, withoutEnlargement: true }).toBuffer();\n const { data, info } = await sharp(grid).greyscale().raw().toBuffer({ resolveWithObject: true });\n const width = info.width, height = info.height;\n if (width < 100 * cols || height < 100 * rows) throw new Error(`Portrait sheet has unusable dimensions ${width}x${height}`);\n\n const plane = { width, height, data: new Uint8Array(data.buffer, data.byteOffset, data.length) };\n const gridCells = detectSheetGrid(plane, cols, rows);\n if (!gridCells.detected) {\n opts.onMismatch?.({\n kind: 'no-dividers',\n message: `no divider lines found on the ${cols}x${rows} sheet; using equal split`,\n detail: { width, height, dividers: describeDividers(plane) },\n });\n } else if (gridCells.cols !== cols || gridCells.rows !== rows) {\n // The model drew a different grid than asked. Its cells are still in row-major\n // order, so they are used as drawn; only characters past the last drawn cell\n // fall back to the equal split.\n opts.onMismatch?.({\n kind: 'different-grid',\n message: `sheet drawn as ${gridCells.cols}x${gridCells.rows}, requested ${cols}x${rows}; using the drawn cells`,\n detail: { width, height, drawnCells: gridCells.cells.length, needed: count },\n });\n }\n const fallback = equalSplitGrid(width, height, cols, rows).cells;\n\n const slices: PortraitSlice[] = [];\n for (let i = 0; i < count; i++) {\n const framing = defaultFraming(gridCells.cells[i] ?? fallback[i]);\n const jpeg = await cutCard(sharp, grid, framing.card);\n slices.push({ key: cells[i].key, label: cells[i].label, jpeg, framing });\n }\n\n let sheetJpeg: Buffer = await sharp(grid).jpeg({ quality: SHEET_JPEG_QUALITY, mozjpeg: true }).toBuffer();\n if (sheetJpeg.length * 4 / 3 > SHEET_MAX_BASE64_BYTES) {\n sheetJpeg = await sharp(grid).jpeg({ quality: 72, mozjpeg: true }).toBuffer();\n }\n return {\n slices,\n sheet: { jpeg: sheetJpeg, width, height, cells: gridCells.cells.slice(0, count), detected: gridCells.detected },\n };\n}\n\n/** Cuts one card out of a stored sheet at the given framing. */\nexport async function cutCard(sharp: SharpLike, sheet: Buffer, card: ImageRect): Promise<Buffer> {\n return sharp(sheet)\n .extract(card)\n .resize(CARD_WIDTH_PX, CARD_HEIGHT_PX)\n .jpeg({ quality: 85 })\n .toBuffer();\n}\n\nexport interface DrawPortraitSheetSpec extends Omit<PortraitSheetPrompt, 'cols' | 'rows'> {\n /** Prompt for the cells that pad the cast out to a full grid. */\n fillerPrompt?: string;\n}\n\nexport interface DrawPortraitSheetOptions extends SliceSheetOptions {\n image?: Pick<GenerateImageOptions, 'model' | 'imageSize' | 'fetchImpl'>;\n}\n\nexport interface DrawnPortraitSheet {\n portraits: PortraitSlice[];\n sheet: DrawnSheet;\n /** What the image call cost, reported for the host's billing. */\n costUSD: number;\n}\n\n/**\n * The whole pipeline: lay the cast out on a grid, draw it in one call, cut the\n * cards. Filler cells are drawn and discarded; `portraits` has exactly one entry\n * per input cell, in order.\n */\nexport async function drawPortraitSheet(apiKey: string, sharp: SharpLike, spec: DrawPortraitSheetSpec, opts: DrawPortraitSheetOptions = {}): Promise<DrawnPortraitSheet> {\n const { cells, cols, rows, realCount } = padCells(spec.cells, spec.fillerPrompt);\n const prompt = buildPortraitSheetPrompt({ cells, cols, rows, purpose: spec.purpose, setting: spec.setting, artStyle: spec.artStyle });\n const image = await generateImage(apiKey, prompt, '4:3', opts.image);\n const { slices, sheet } = await sliceSheet(sharp, image.buffer, cells, realCount, cols, rows, opts);\n return { portraits: slices, sheet, costUSD: image.costUSD };\n}\n"],"mappings":"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","names":[]}
|
package/dist/images.mjs
CHANGED
|
@@ -201,7 +201,8 @@ function calculateImageCost(model, imageOutputTokens, textInputTokens) {
|
|
|
201
201
|
|
|
202
202
|
// src/images/google-image.ts
|
|
203
203
|
var ImageRefusalError = class extends Error {
|
|
204
|
-
/** The block code as Gemini sent it (a URI or bare `snake_case`
|
|
204
|
+
/** The block code as Gemini sent it: an `errors[].code` (a URI or bare `snake_case`
|
|
205
|
+
* name) on the HTTP 200 shape, or `http_400` on the error-body shape. */
|
|
205
206
|
code;
|
|
206
207
|
/** Gemini's human-readable message, when it sent one. */
|
|
207
208
|
reason;
|
|
@@ -218,6 +219,7 @@ function isImageRefusalError(error) {
|
|
|
218
219
|
return !!error && typeof error === "object" && error.name === "ImageRefusalError";
|
|
219
220
|
}
|
|
220
221
|
var BLOCK_CODE_MARKERS = ["safety", "prohibited", "recitation", "blocklist", "spii", "content_blocked", "image_other"];
|
|
222
|
+
var BLOCK_TEXT = /blocked|safety violation|prohibited|filtered out|prohibited use policy/i;
|
|
221
223
|
function describeBlockCode(code) {
|
|
222
224
|
const tail = code.split(/[#/]/).filter(Boolean).pop() ?? code;
|
|
223
225
|
return tail.toLowerCase();
|
|
@@ -226,6 +228,9 @@ function isBlockCode(code) {
|
|
|
226
228
|
const tail = describeBlockCode(code);
|
|
227
229
|
return BLOCK_CODE_MARKERS.some((marker) => tail.includes(marker));
|
|
228
230
|
}
|
|
231
|
+
function trimReason(text) {
|
|
232
|
+
return text.replace(/\s*\[[^\]]*\]\([^)]*\)[^.]*\.?/g, "").trim().slice(0, 300);
|
|
233
|
+
}
|
|
229
234
|
async function generateImage(apiKey, prompt, aspectRatio, opts = {}) {
|
|
230
235
|
const model = opts.model ?? DEFAULT_IMAGE_MODEL;
|
|
231
236
|
const input = [{ type: "text", text: prompt }];
|
|
@@ -244,7 +249,7 @@ async function generateImage(apiKey, prompt, aspectRatio, opts = {}) {
|
|
|
244
249
|
})
|
|
245
250
|
});
|
|
246
251
|
if (!res.ok) {
|
|
247
|
-
throw
|
|
252
|
+
throw httpError(model, res.status, await res.text());
|
|
248
253
|
}
|
|
249
254
|
const json = await res.json();
|
|
250
255
|
const b64 = (json.steps || []).flatMap((s) => s.content || []).find((c) => c.type === "image" && c.data)?.data;
|
|
@@ -254,16 +259,39 @@ async function generateImage(apiKey, prompt, aspectRatio, opts = {}) {
|
|
|
254
259
|
const inputTokens = usage.total_input_tokens || 0;
|
|
255
260
|
return { buffer: Buffer.from(b64, "base64"), costUSD: calculateImageCost(model, imageTokens, inputTokens) };
|
|
256
261
|
}
|
|
262
|
+
function httpError(model, status, body) {
|
|
263
|
+
if (status === 400) {
|
|
264
|
+
let error;
|
|
265
|
+
try {
|
|
266
|
+
error = JSON.parse(body)?.error;
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
const message = typeof error?.message === "string" ? error.message : "";
|
|
270
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
271
|
+
if (BLOCK_TEXT.test(message) || BLOCK_TEXT.test(code)) {
|
|
272
|
+
return new ImageRefusalError(model, "http_400", trimReason(message || code));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return new Error(`Image request failed: HTTP ${status} ${body.slice(0, 600)}`);
|
|
276
|
+
}
|
|
257
277
|
function imagelessReplyError(model, json) {
|
|
258
278
|
const errors = Array.isArray(json?.errors) ? json.errors : [];
|
|
259
279
|
const codes = errors.map((e) => String(e?.code ?? "")).filter(Boolean);
|
|
260
280
|
const messages = errors.map((e) => typeof e?.message === "string" ? e.message.trim() : "").filter(Boolean);
|
|
261
281
|
const block = codes.find(isBlockCode);
|
|
262
282
|
if (block) {
|
|
263
|
-
return new ImageRefusalError(model, block, messages[0]
|
|
283
|
+
return new ImageRefusalError(model, block, messages[0] ? trimReason(messages[0]) : void 0);
|
|
284
|
+
}
|
|
285
|
+
const text = (json?.steps || []).flatMap((s) => s.content || []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text.trim()).join(" ").slice(0, 300);
|
|
286
|
+
if (!codes.length && BLOCK_TEXT.test(text)) {
|
|
287
|
+
return new ImageRefusalError(model, "text_block", trimReason(text));
|
|
264
288
|
}
|
|
265
289
|
const status = typeof json?.status === "string" ? json.status : "unknown";
|
|
266
|
-
const detail = [
|
|
290
|
+
const detail = [
|
|
291
|
+
codes.length ? `code ${codes.map(describeBlockCode).join(", ")}` : "",
|
|
292
|
+
messages[0]?.slice(0, 300) ?? "",
|
|
293
|
+
text ? `text "${text}"` : ""
|
|
294
|
+
].filter(Boolean).join("; ");
|
|
267
295
|
return new Error(`Image response contained no image data (status ${status}${detail ? `; ${detail}` : ""})`);
|
|
268
296
|
}
|
|
269
297
|
|