@hiper2d/ai-agents 0.26.1 → 0.26.3

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../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 * 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// Darkness alone stopped being enough with Nano Banana 2.1 (2026-10-21): its\n// \"muted\" backgrounds measured 40-58 on the grey plane, so whole cells read as\n// dark and a 4x3 sheet came back as 5x3 with the cast cut through the seams\n// (a divider at x=1800 was swallowed by a 200 px dark run and dropped as too\n// thick, while a walking stick and two patches of navy background passed as\n// lines). A line pixel must therefore also be darker than BOTH flanks of its\n// scanline — the mean over a window this far out on each side, outside any\n// line (lines are ≤ ~9 px) — by at least LINE_CONTRAST. Lines are near-black\n// (0-30), backgrounds ≥ 40 right next to them; a dark object inside a cell\n// (stick, hair, hood edge) has a dark flank on at least one side.\nconst LINE_CONTRAST = 20;\nconst FLANK_FROM = 6;\nconst FLANK_TO = 15;\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/** Divider 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.\n *\n * Two readings are merged. Dark runs (every scanline mostly below DARK_MAX)\n * are kept when the run as a whole stands out from the plane just outside it\n * — that is the thin line, and the line with a name plate hugging it. Thin\n * contrast runs (scanlines darker than both their flanks at a fixed distance)\n * find a line that a dark background or a dark cast has fused into one run\n * too thick to be a boundary. */\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 span = to - from;\n // Pixel k of scanline i (k runs across the window, i along the axis).\n const at = axis === 'rows'\n ? (i: number, k: number) => data[i * width + k]\n : (i: number, k: number) => data[k * width + i];\n const clamp = (i: number) => Math.min(extent - 1, Math.max(0, i));\n const flankMean = (i: number, k: number, dir: -1 | 1, start: number, end: number): number => {\n let sum = 0;\n for (let d = start; d <= end; d++) sum += at(clamp(i + dir * d), k);\n return sum / (end - start + 1);\n };\n const maxThickness = extent * DIVIDER_MAX_THICKNESS_SHARE;\n const lineShaped = (r: Run) => {\n const t = r.end - r.start + 1;\n return r.start > 2 && r.end < extent - 3 && t >= DIVIDER_MIN_THICKNESS && t <= maxThickness;\n };\n\n const dark: number[] = [];\n const contrast: number[] = [];\n for (let i = 0; i < extent; i++) {\n let darkCount = 0, contrastCount = 0;\n for (let k = from; k < to; k++) {\n const v = at(i, k);\n if (v >= DARK_MAX) continue;\n darkCount++;\n const flank = Math.min(flankMean(i, k, -1, FLANK_FROM, FLANK_TO), flankMean(i, k, 1, FLANK_FROM, FLANK_TO));\n if (v < flank - LINE_CONTRAST) contrastCount++;\n }\n if (darkCount / span > DIVIDER_SHARE) dark.push(i);\n if (contrastCount / span > DIVIDER_SHARE) contrast.push(i);\n }\n\n // Dark runs that stand out from what lies just outside them: the run's\n // mean across its scanlines (a soft stroke has no single dark centre)\n // against the flanks beyond either end. The brighter flank decides: a row\n // line has the previous row's shoulders (often dark clothing) right above\n // it, and the next row's headroom below — measured 0.54-0.82 of the width\n // against the darker flank, 0.98+ against the brighter; the false runs\n // (stick, background patch, hood) reach 0.69.\n const standsOut = (r: Run) => {\n const thickness = r.end - r.start + 1;\n const before = r.start - 1, after = r.end + 1;\n let count = 0;\n for (let k = from; k < to; k++) {\n let sum = 0;\n for (let i = r.start; i <= r.end; i++) sum += at(i, k);\n const flank = Math.max(flankMean(before, k, -1, FLANK_FROM - 1, FLANK_TO - 1), flankMean(after, k, 1, FLANK_FROM - 1, FLANK_TO - 1));\n if (sum / thickness < flank - LINE_CONTRAST) count++;\n }\n return count / span > DIVIDER_SHARE;\n };\n const kept = runs(dark).filter(lineShaped).filter(standsOut);\n\n // Contrast runs, added where no kept run already covers them. They are not\n // widened over neighbouring dark scanlines: with a dark background that\n // would push the cut into the cell, and the inset already clears the\n // stroke's anti-aliased edge.\n for (const seed of runs(contrast).filter(lineShaped)) {\n if (kept.some(k => k.start <= seed.end && seed.start <= k.end)) continue;\n kept.push(seed);\n }\n return kept.sort((a, b) => a.start - b.start);\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 from\n * ai.google.dev/gemini-api/docs/pricing.\n *\n * Nano Banana 2.1 (default since 0.26.0, 2026-10-20): image output bills by size — 1120\n * tokens at 1K, 1680 at 2K, 3780 at 4K — and the model thinks before drawing, which bills\n * as TEXT output (~650-750 thought tokens at the default `medium` level, none at `minimal`;\n * measured live 2026-10-20). One 1K image ≈ $0.040, one 2K image ≈ $0.058.\n *\n * Gemini 3.1 Flash Image (the previous default, kept selectable) bills a flat ~1120 image\n * tokens regardless of size, ≈ $0.067 an image.\n */\nexport const IMAGE_MODEL_CONSTANTS = {\n GEMINI_NANO_BANANA: 'gemini-nano-banana-2.1',\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 /** Text and thinking output. Absent = the model bills none (or we never measured it). */\n textOutputPricePerM?: number;\n}\n\nexport const IMAGE_MODEL_PRICING: Record<ImageModelId, ImageModelPricing> = {\n [IMAGE_MODEL_CONSTANTS.GEMINI_NANO_BANANA]: {\n imageOutputPricePerM: 30,\n textInputPricePerM: 1.5,\n textOutputPricePerM: 7.5,\n },\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_NANO_BANANA;\n\n/**\n * Cost of one image call from the usage Gemini reports. `textOutputTokens` is the text\n * the model wrote next to the image plus its thought tokens.\n */\nexport function calculateImageCost(model: ImageModelId, imageOutputTokens: number, textInputTokens: number, textOutputTokens: number = 0): 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 textOutputTokens / 1_000_000 * (pricing.textOutputPricePerM ?? 0)\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' | '4K';\n model?: ImageModelId;\n /** How much the model thinks before drawing (Nano Banana 2.1; `medium` is the API\n * default). Thought tokens bill as text output, so `minimal` is the cheap draft mode. */\n thinkingLevel?: 'minimal' | 'medium' | 'high';\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. The usage block\n * reports image tokens by modality, the text the model wrote inside `total_output_tokens`,\n * and thinking apart as `total_thought_tokens` (shape verified live 2026-10-20).\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 ...(opts.thinkingLevel ? { generation_config: { thinking_level: opts.thinkingLevel } } : {}),\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 const textTokens = Math.max(0, (usage.total_output_tokens || 0) - imageTokens) + (usage.total_thought_tokens || 0);\n\n return { buffer: Buffer.from(b64, 'base64'), costUSD: calculateImageCost(model, imageTokens, inputTokens, textTokens) };\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. Never 3x3 either (2026-10-18): on the 4:3 canvas its cells are\n * landscape (800x600) and the model kept redrawing the 9-cell sheet as 4x3 with\n * square cells — the smallest game (7 bots + human + GM = 9) tripped\n * AVATAR_GRID_MISMATCH every time. 9 cells now get 4x3 with three fillers.\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 <= 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' | 'thinkingLevel' | '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":["clamp"]}
1
+ {"version":3,"sources":["../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 * 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// Darkness alone stopped being enough with Nano Banana 2.1 (2026-10-21): its\n// \"muted\" backgrounds measured 40-58 on the grey plane, so whole cells read as\n// dark and a 4x3 sheet came back as 5x3 with the cast cut through the seams\n// (a divider at x=1800 was swallowed by a 200 px dark run and dropped as too\n// thick, while a walking stick and two patches of navy background passed as\n// lines). A line pixel must therefore also be darker than BOTH flanks of its\n// scanline — the mean over a window this far out on each side, outside any\n// line (lines are ≤ ~9 px) — by at least LINE_CONTRAST. Lines are near-black\n// (0-30), backgrounds ≥ 40 right next to them; a dark object inside a cell\n// (stick, hair, hood edge) has a dark flank on at least one side.\nconst LINE_CONTRAST = 20;\nconst FLANK_FROM = 6;\nconst FLANK_TO = 15;\n// The contrast test fails the other way on a dark art style (a space station,\n// a polar night, 2026-10-21): lines of 1-30 between backgrounds of 30-50 are\n// not 20 darker than their flanks, and a 5x3 sheet read as 2x1. So the\n// requested grid is tried FIRST: the model draws equal cells, and when every\n// equal-split position has a dark line within this share of the cell pitch,\n// those lines are the grid — no contrast needed. Only a sheet that fails\n// that check goes through the general line search below.\nconst GRID_LINE_TOLERANCE = 0.08;\nconst GRID_LINE_SHARE = 0.9;\n// A scanline next to a confirmed line belongs to it only when its mean is\n// within this of the line's: a name plate (15 vs a 20 line) does, the dark\n// top of a space-station cell (40 vs a 2 line) does not.\nconst LINE_MEAN_MARGIN = 15;\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/** Divider 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.\n *\n * Two readings are merged. Dark runs (every scanline mostly below DARK_MAX)\n * are kept when the run as a whole stands out from the plane just outside it\n * — that is the thin line, and the line with a name plate hugging it. Thin\n * contrast runs (scanlines darker than both their flanks at a fixed distance)\n * find a line that a dark background or a dark cast has fused into one run\n * too thick to be a boundary. */\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 span = to - from;\n // Pixel k of scanline i (k runs across the window, i along the axis).\n const at = axis === 'rows'\n ? (i: number, k: number) => data[i * width + k]\n : (i: number, k: number) => data[k * width + i];\n const clamp = (i: number) => Math.min(extent - 1, Math.max(0, i));\n const flankMean = (i: number, k: number, dir: -1 | 1, start: number, end: number): number => {\n let sum = 0;\n for (let d = start; d <= end; d++) sum += at(clamp(i + dir * d), k);\n return sum / (end - start + 1);\n };\n const maxThickness = extent * DIVIDER_MAX_THICKNESS_SHARE;\n const lineShaped = (r: Run) => {\n const t = r.end - r.start + 1;\n return r.start > 2 && r.end < extent - 3 && t >= DIVIDER_MIN_THICKNESS && t <= maxThickness;\n };\n\n const dark: number[] = [];\n const contrast: number[] = [];\n for (let i = 0; i < extent; i++) {\n let darkCount = 0, contrastCount = 0;\n for (let k = from; k < to; k++) {\n const v = at(i, k);\n if (v >= DARK_MAX) continue;\n darkCount++;\n const flank = Math.min(flankMean(i, k, -1, FLANK_FROM, FLANK_TO), flankMean(i, k, 1, FLANK_FROM, FLANK_TO));\n if (v < flank - LINE_CONTRAST) contrastCount++;\n }\n if (darkCount / span > DIVIDER_SHARE) dark.push(i);\n if (contrastCount / span > DIVIDER_SHARE) contrast.push(i);\n }\n\n // Dark runs that stand out from what lies just outside them: the run's\n // mean across its scanlines (a soft stroke has no single dark centre)\n // against the flanks beyond either end. The brighter flank decides: a row\n // line has the previous row's shoulders (often dark clothing) right above\n // it, and the next row's headroom below — measured 0.54-0.82 of the width\n // against the darker flank, 0.98+ against the brighter; the false runs\n // (stick, background patch, hood) reach 0.69.\n const standsOut = (r: Run) => {\n const thickness = r.end - r.start + 1;\n const before = r.start - 1, after = r.end + 1;\n let count = 0;\n for (let k = from; k < to; k++) {\n let sum = 0;\n for (let i = r.start; i <= r.end; i++) sum += at(i, k);\n const flank = Math.max(flankMean(before, k, -1, FLANK_FROM - 1, FLANK_TO - 1), flankMean(after, k, 1, FLANK_FROM - 1, FLANK_TO - 1));\n if (sum / thickness < flank - LINE_CONTRAST) count++;\n }\n return count / span > DIVIDER_SHARE;\n };\n const kept = runs(dark).filter(lineShaped).filter(standsOut);\n\n // Contrast runs, added where no kept run already covers them. They are not\n // widened over neighbouring dark scanlines: with a dark background that\n // would push the cut into the cell, and the inset already clears the\n // stroke's anti-aliased edge.\n for (const seed of runs(contrast).filter(lineShaped)) {\n if (kept.some(k => k.start <= seed.end && seed.start <= k.end)) continue;\n kept.push(seed);\n }\n return kept.sort((a, b) => a.start - b.start);\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\ninterface Scanlines { shares: Float32Array; means: Float32Array }\n\n/** Per scanline along one axis, over the full extent of the other: the share\n * of dark pixels and the mean value. */\nfunction scanlines(plane: GreyPlane, axis: 'rows' | 'cols'): Scanlines {\n const {width, height, data} = plane;\n const extent = axis === 'rows' ? height : width;\n const span = axis === 'rows' ? width : height;\n const shares = new Float32Array(extent), means = new Float32Array(extent);\n for (let i = 0; i < extent; i++) {\n let count = 0, sum = 0;\n if (axis === 'rows') {\n const base = i * width;\n for (let x = 0; x < width; x++) { const v = data[base + x]; sum += v; if (v < DARK_MAX) count++; }\n } else {\n for (let y = 0; y < height; y++) { const v = data[y * width + i]; sum += v; if (v < DARK_MAX) count++; }\n }\n shares[i] = count / span;\n means[i] = sum / span;\n }\n return {shares, means};\n}\n\n/** The dark line nearest an expected position, within the tolerance, or\n * null. The run is the darkest scanline plus its neighbours that are as\n * dark as it, near enough — the stroke and a name plate hugging it, not the\n * dark cell top beside it. Where even that is too thick to be a line (a\n * black art style all around) the line is a 7 px stroke at the darkest\n * scanline. */\nfunction lineNear(lines: Scanlines, position: number, tolerance: number, maxThickness: number): Run | null {\n const {shares, means} = lines;\n const extent = shares.length;\n const lo = Math.max(3, Math.round(position - tolerance));\n const hi = Math.min(extent - 4, Math.round(position + tolerance));\n let at = -1;\n for (let i = lo; i <= hi; i++) if (at < 0 || shares[i] > shares[at] || (shares[i] === shares[at] && means[i] < means[at])) at = i;\n if (at < 0 || shares[at] < GRID_LINE_SHARE) return null;\n const partOfLine = (i: number) => shares[i] >= GRID_LINE_SHARE && means[i] <= means[at] + LINE_MEAN_MARGIN;\n let start = at, end = at;\n while (start - 1 >= 3 && partOfLine(start - 1)) start--;\n while (end + 1 <= extent - 4 && partOfLine(end + 1)) end++;\n if (end - start + 1 > maxThickness) return {start: Math.max(3, at - 3), end: Math.min(extent - 4, at + 3)};\n return {start, end};\n}\n\n/** The requested grid confirmed on the pixels: a dark line near every\n * equal-split position on both axes. Null when any line is missing. */\nexport function confirmGrid(plane: GreyPlane, cols: number, rows: number): SheetGrid | null {\n const {width, height} = plane;\n const find = (axis: 'rows' | 'cols', count: number): Run[] | null => {\n const extent = axis === 'rows' ? height : width;\n const scan = scanlines(plane, axis);\n const pitch = extent / count;\n const lines: Run[] = [];\n for (let k = 1; k < count; k++) {\n const line = lineNear(scan, k * pitch, pitch * GRID_LINE_TOLERANCE, extent * DIVIDER_MAX_THICKNESS_SHARE);\n if (!line) return null;\n lines.push(line);\n }\n return lines;\n };\n const colLines = find('cols', cols);\n if (!colLines) return null;\n const rowLines = find('rows', rows);\n if (!rowLines) return null;\n const cells: ImageRect[] = [];\n for (const band of spans(rowLines, height)) {\n for (const x of spans(colLines, width)) {\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 return {cells, cols, rows, detected: true};\n}\n\n/**\n * The sheet's cells. The requested grid is confirmed on the pixels first\n * (`confirmGrid`); the model draws it as asked far more often than not, and\n * that check needs no assumptions about the art style. Otherwise the divider\n * lines the model drew are read wherever they form a plausible grid, even\n * when that grid differs from the cols x rows that were asked for — the\n * model keeps row-major order, so the drawn cells still map to the\n * characters in sequence. Row lines are found over the full width; column\n * lines are found PER ROW BAND, because the model has drawn rows with\n * different column layouts (and a column line that stops short of the last\n * row). A band without lines of its own borrows the layout of the band\n * above it. The equal split of the requested grid is the last resort when\n * no lines are found at all (the caller logs it; the kept sheet lets the\n * owner reframe by hand either way).\n */\nexport function detectSheetGrid(plane: GreyPlane, cols: number, rows: number): SheetGrid {\n const confirmed = confirmGrid(plane, cols, rows);\n if (confirmed) return confirmed;\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 from\n * ai.google.dev/gemini-api/docs/pricing.\n *\n * Nano Banana 2.1 (default since 0.26.0, 2026-10-20): image output bills by size — 1120\n * tokens at 1K, 1680 at 2K, 3780 at 4K — and the model thinks before drawing, which bills\n * as TEXT output (~650-750 thought tokens at the default `medium` level, none at `minimal`;\n * measured live 2026-10-20). One 1K image ≈ $0.040, one 2K image ≈ $0.058.\n *\n * Gemini 3.1 Flash Image (the previous default, kept selectable) bills a flat ~1120 image\n * tokens regardless of size, ≈ $0.067 an image.\n */\nexport const IMAGE_MODEL_CONSTANTS = {\n GEMINI_NANO_BANANA: 'gemini-nano-banana-2.1',\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 /** Text and thinking output. Absent = the model bills none (or we never measured it). */\n textOutputPricePerM?: number;\n}\n\nexport const IMAGE_MODEL_PRICING: Record<ImageModelId, ImageModelPricing> = {\n [IMAGE_MODEL_CONSTANTS.GEMINI_NANO_BANANA]: {\n imageOutputPricePerM: 30,\n textInputPricePerM: 1.5,\n textOutputPricePerM: 7.5,\n },\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_NANO_BANANA;\n\n/**\n * Cost of one image call from the usage Gemini reports. `textOutputTokens` is the text\n * the model wrote next to the image plus its thought tokens.\n */\nexport function calculateImageCost(model: ImageModelId, imageOutputTokens: number, textInputTokens: number, textOutputTokens: number = 0): 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 textOutputTokens / 1_000_000 * (pricing.textOutputPricePerM ?? 0)\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' | '4K';\n model?: ImageModelId;\n /** How much the model thinks before drawing (Nano Banana 2.1; `medium` is the API\n * default). Thought tokens bill as text output, so `minimal` is the cheap draft mode. */\n thinkingLevel?: 'minimal' | 'medium' | 'high';\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. The usage block\n * reports image tokens by modality, the text the model wrote inside `total_output_tokens`,\n * and thinking apart as `total_thought_tokens` (shape verified live 2026-10-20).\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 ...(opts.thinkingLevel ? { generation_config: { thinking_level: opts.thinkingLevel } } : {}),\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 const textTokens = Math.max(0, (usage.total_output_tokens || 0) - imageTokens) + (usage.total_thought_tokens || 0);\n\n return { buffer: Buffer.from(b64, 'base64'), costUSD: calculateImageCost(model, imageTokens, inputTokens, textTokens) };\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. Never 3x3 either (2026-10-18): on the 4:3 canvas its cells are\n * landscape (800x600) and the model kept redrawing the 9-cell sheet as 4x3 with\n * square cells — the smallest game (7 bots + human + GM = 9) tripped\n * AVATAR_GRID_MISMATCH every time. 9 cells now get 4x3 with three fillers.\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 <= 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' | 'thinkingLevel' | '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":["clamp"]}
package/dist/index.js CHANGED
@@ -1115,7 +1115,9 @@ var SupportedAiModels = {
1115
1115
  reasoningEffort: "low",
1116
1116
  // Reasoning tokens share the output budget, so leave room for both CoT and answer.
1117
1117
  maxOutputTokens: 65536,
1118
- tags: ["cheap"]
1118
+ // slow: werewolf prod 2026-10-21, 785 calls over 30 days, p50 10s, p90 31s, max 156s;
1119
+ // 7 of 79 bot turns past the 60s timeout in a 16-player game at days 8-9.
1120
+ tags: ["slow", "cheap"]
1119
1121
  },
1120
1122
  // Models with always-on reasoning
1121
1123
  // GPT-6 Astra (2026-09-03): OpenAI's frontier tier above Sol. No `none` reasoning effort;
@@ -1195,7 +1197,9 @@ var SupportedAiModels = {
1195
1197
  apiKeyName: API_KEY_CONSTANTS.GOOGLE,
1196
1198
  hasThinking: true,
1197
1199
  reasoningEffort: "medium",
1198
- tags: ["expensive"]
1200
+ // slow: werewolf prod 2026-10-21, 193 calls over 30 days, p50 12s, p90 34s, max 207s;
1201
+ // 3 of 50 night actions past the 60s timeout even at medium effort.
1202
+ tags: ["slow", "expensive"]
1199
1203
  },
1200
1204
  [LLM_CONSTANTS.GEMINI_PRO]: {
1201
1205
  displayName: "Gemini 3.1 Pro Preview",
@@ -1246,7 +1250,10 @@ var SupportedAiModels = {
1246
1250
  apiKeyName: API_KEY_CONSTANTS.GROK,
1247
1251
  hasThinking: true,
1248
1252
  reasoningEffort: "medium",
1249
- temperature: 0.7
1253
+ temperature: 0.7,
1254
+ // very-slow: werewolf prod 2026-10-21, 29 calls over 30 days, p50 48s, p90 192s, max 244s
1255
+ // at medium effort; the 15-character story pipeline took 213s on 2026-10-07.
1256
+ tags: ["very-slow"]
1250
1257
  },
1251
1258
  // Mistral models. One hybrid entry: reasoning is off by default on the API and switched on
1252
1259
  // per request with `reasoning_effort` (see mistral-agent.ts); the trace comes back as a