@goodandready/dsh-image-gen 0.9.0 → 0.10.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 CHANGED
@@ -31,24 +31,35 @@
31
31
 
32
32
  ---
33
33
 
34
+ ## 🛡️ Reliability, Performance & Quality (v0.10.0)
35
+
36
+ * **Exponential Backoff with Jitter**: Adaptive polling for FAL, Replicate, and ComfyUI queues protects against HTTP 429 rate limits.
37
+ * **Error Classification in Fallback Cascade**: Client-side errors (Content Policy, 400 Bad Request, NSFW) fail fast without wasting API credits on other providers.
38
+ * **Deterministic Hash Caching**: Exact matches of prompt, model, and seed return instantly from local storage with zero API expense.
39
+ * **ComfyUI & Automatic1111 Drag-and-Drop**: Metadata is packed into PNG `Parameters` chunks in standard format.
40
+ * **Dimension Snapping**: Automatic normalization to multiples of 64 guarantees VAE bucket compatibility.
41
+ * **Enhanced Style Presets**: Built-in styles include tailor-made negative prompts and optimal guidance scale settings.
42
+
43
+ ---
44
+
34
45
  ## 🛠️ Complete Tools Reference
35
46
 
36
47
  * **`generate_image`**: Generate images with pluggable providers, seeds, aspect ratios, and style presets.
37
48
  * **`remove_background`**: Extract subject with transparent PNG output (FAL BiRefNet / Rembg).
38
49
  * **`upscale_image`**: 2x / 4x super-resolution with clarity reconstruction.
39
- * **`vectorize_image`**: Convert raster graphics to clean scalable SVG vectors.
50
+ * **`vectorize_image`**: Convert raster graphics to clean scalable SVG vectors with palette quantization.
40
51
  * **`blend_images`**: Multi-reference composition mixing.
41
- * **`generate_image_pack`**: Simultaneous multi-aspect ratio rendering (1:1, 16:9, 9:16).
52
+ * **`generate_image_pack`**: Simultaneous multi-aspect ratio rendering with graceful partial recovery.
42
53
  * **`compare_images`**: Pixel-level visual difference ratio comparison.
43
- * **`inspect_image_quality`**: Automated visual audit and defect detection.
54
+ * **`inspect_image_quality`**: Automated visual audit, Laplacian sharpness scoring, and defect detection.
44
55
 
45
56
  ---
46
57
 
47
58
  ## 🎨 Supported Generation Backends
48
59
 
49
- * **`fal`** (Default): High-speed queue for FLUX.1, SDXL, Clarity Upscaler, and BiRefNet.
60
+ * **`fal`** (Default): FAL.ai queue for FLUX.1, SDXL, Clarity Upscaler, and BiRefNet.
50
61
  * **`replicate`**: FLUX and SDXL models via Replicate API.
51
- * **`custom`**: OpenAI-compatible endpoint (DALL-E 3, SiliconFlow, Together AI, local ComfyUI).
62
+ * **`custom`**: OpenAI-compatible endpoint (DALL-E 3, SiliconFlow, Together AI, local gateways).
52
63
  * **`codex`**: ChatGPT Plus/Pro subscription generation via `dsh-subscriptions` (OAuth).
53
64
  * **`grok`**: Grok Imagine subscription generation via `dsh-subscriptions` (OAuth).
54
65
  * **`local`**: Local ComfyUI workflow execution or Automatic1111 web API.
package/README.ru.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  ## ⚡ Обзор возможностей
29
29
 
30
- **`@goodandready/dsh-image-gen`** — флагманский графический плагин для экосистемы DeepSeek Harness, превращающий агента в полноценную творческую студию. Плагин предоставляет богатый набор инструментов для генерации, трансформации, апскейла, векторизации и анализа изображений с поддержкой 8 провайдеров генерации и интерактивной карточкой в чате.
30
+ **`@goodandready/dsh-image-gen`** — флагманский графический плагин для экосистемы DeepSeek Harness, превращающий агента в полноценную творческую студию. Плагин предоставляет богатый набор инструментов для генерации, трансформации, апскейла, векторизации и анализа изображений с поддержкой 8 провайдеров генерации, интеллектуальным кэшированием и интерактивной карточкой в чате.
31
31
 
32
32
  ```mermaid
33
33
  graph LR
@@ -47,8 +47,14 @@ graph LR
47
47
  T8[inspect_image_quality]
48
48
  end
49
49
 
50
+ subgraph Core [🛡️ Ядро надежности и кэша]
51
+ Cache[Детерминированный sha256 Hash Cache]
52
+ Backoff[Exponential Backoff + Jitter]
53
+ Classifier[Классификатор фатальных ошибок 400]
54
+ Snap64[VAE 64-Multiple Dimension Snapping]
55
+ end
56
+
50
57
  subgraph Engine [⚙️ Диспетчер бэкендов]
51
- Router{Умный маршрутизатор}
52
58
  P_FAL[FAL.ai Queue]
53
59
  P_REP[Replicate API]
54
60
  P_CUST[OpenAI / SiliconFlow]
@@ -60,19 +66,19 @@ graph LR
60
66
  end
61
67
 
62
68
  subgraph Store [💾 Хранение и UI]
63
- Sidecar[.json Sidecar + PNG tEXt]
64
- Attach[ctx.attachments & Disk]
65
- Card[🖼️ Карточка в чате с Re-roll, Upscale, NoBG]
69
+ Sidecar[.json Sidecar + Parameters tEXt]
70
+ A1111[Drag-and-Drop в ComfyUI / WebUI]
71
+ Card[🖼️ Карточка в чате с Re-roll, Правкой, NoBG]
66
72
  end
67
73
 
68
74
  Input --> Tools
69
- Tools --> Router
70
- Router --> P_FAL & P_REP & P_CUST & P_COD & P_GROK & P_LOC & P_SEA & P_GEM
71
- P_FAL & P_REP & P_CUST & P_COD & P_GROK & P_LOC & P_SEA & P_GEM --> Sidecar
72
- Sidecar --> Attach --> Card
75
+ Tools --> Core
76
+ Core --> Engine
77
+ Engine --> Store
73
78
 
74
79
  style Input fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
75
80
  style Tools fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
81
+ style Core fill:#11111b,stroke:#fab387,stroke-width:2px,color:#cdd6f4
76
82
  style Engine fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
77
83
  style Store fill:#1e1e2e,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
78
84
  ```
@@ -86,52 +92,35 @@ graph LR
86
92
  | **`generate_image`** | Генерация изображения по тексту | `prompt`, `image_size`, `seed`, `style`, `negative_prompt`, `count` |
87
93
  | **`remove_background`** | Удаление фона с сохранением прозрачного PNG | `image`, `model`, `output_name` |
88
94
  | **`upscale_image`** | Увеличение разрешения в 2x / 4x с детализацией | `image`, `scale` (2/4), `prompt`, `creativity` |
89
- | **`vectorize_image`** | Векторизация растра в чистый SVG | `image`, `color_mode` (`color`/`binary`) |
95
+ | **`vectorize_image`** | Векторизация растра в чистый SVG с квантованием палитры | `image`, `color_mode`, `paletteSize` |
90
96
  | **`blend_images`** | Смешивание нескольких изображений в единую композицию | `images`, `weights`, `prompt` |
91
- | **`generate_image_pack`** | Пакетный рендеринг форматов 1:1, 16:9, 9:16 под соцсети | `prompt`, `aspect_ratios` |
97
+ | **`generate_image_pack`** | Пакетный рендеринг форматов 1:1, 16:9, 9:16 с сохранением частичных результатов | `prompt`, `aspect_ratios` |
92
98
  | **`compare_images`** | Сравнение двух картинок (доля различий пикселей) | `image_a`, `image_b` |
93
- | **`inspect_image_quality`** | Аудит качества и артефактов генерации | `image`, `expected_elements` |
94
-
95
- ---
96
-
97
- ## 🎨 Поддерживаемые провайдеры генерации
98
-
99
- | Провайдер | Бэкенд | Ключи / Доступ | Назначение и особенности |
100
- |---|---|---|---|
101
- | **`fal`** *(дефолт)* | FAL.ai Queue | `FAL_API_KEY` | Сверхбыстрая генерация (`FLUX.1-schnell`, `FLUX-dev`, `SDXL`), Upscaler, BiRefNet |
102
- | **`replicate`** | Replicate API | `REPLICATE_API_TOKEN` | Модели сообщества (`black-forest-labs/flux-schnell`, `stability-ai/sdxl`) |
103
- | **`custom`** | OpenAI-совместимый эндпоинт | `OPENAI_API_KEY` | DALL-E 3, SiliconFlow, Together AI, локальный OpenAI шлюз |
104
- | **`codex`** | ChatGPT Plus/Pro | *Без ключа (OAuth)* | Использование учетной записи из `dsh-subscriptions` без тарификации API |
105
- | **`grok`** | Grok Imagine | *Без ключа (OAuth)* | Использование учетной записи Grok из `dsh-subscriptions` без тарификации API |
106
- | **`local`** | Локальный сервер | *Не требуется* | ComfyUI (граф нод / prompt queue) или Automatic1111 (txt2img/img2img) |
107
- | **`seedream`** | ByteDance Doubao / SeaDream | `SEEDREAM_API_KEY` | Азиатские фотореалистичные генеративные модели |
108
- | **`gemini`** | Google GenAI API | `GEMINI_API_KEY` | Imagen 3 через официальный Gemini API |
99
+ | **`inspect_image_quality`** | Аудит качества, расчет резкости и проверка дефектов | `image`, `expected_elements` |
109
100
 
110
101
  ---
111
102
 
112
- ## 🎭 Библиотека стилей (`style_presets`)
103
+ ## 🛡️ Отказоустойчивость, производительность и качество (v0.10.0)
113
104
 
114
- Плагин содержит встроенные профили стилизации:
115
- * **`cinematic`**: Кинематографичный кадр, 35mm оптика, естественный свет и глубина резкости.
116
- * **`anime`**: Аниме-эстетика в стиле Макото Синкая, сочные цвета, тонкий лайн-арт.
117
- * **`isometric`**: Изометрический 3D-рендер, пластилиновый стиль, мягкий студийный свет.
118
- * **`cyberpunk`**: Киберпанк, неоновые отражения, объемный туман, ночная атмосфера.
119
- * **`pixel_art`**: 16-битный ретро пиксель-арт, четкие контуры, дизеринг.
120
- * **`oil_painting`**: Классическая масляная живопись, фактура холста, пастозные мазки.
121
- * **`minimalist`**: Минималистичный плоский вектор, чистые линии, строгая геометрия.
105
+ * **Экспоненциальный Backoff с Jitter**: опрос очередей генерации (FAL, Replicate, ComfyUI) автоматически адаптирует задержки, исключая ошибки `429 Too Many Requests`.
106
+ * **Классификатор ошибок в Fallback Cascade**: клиентские ошибки (Content Policy, 400 Bad Request, NSFW) отсекаются немедленно, предотвращая пустую трату денег на резервных провайдерах.
107
+ * **Детерминированный хэш-кэш**: повторные генерации с тем же промптом, моделью и сидом отдаются мгновенно из локального кэша с нулевой стоимостью API.
108
+ * **Совместимость с ComfyUI / Automatic1111**: метаданные вшиваются в PNG `tEXt` чанки в стандартном формате `Parameters: Prompt\nNegative prompt: ...\nSteps: ... Seed: ...`, обеспечивая нативный Drag-and-Drop.
109
+ * **Кратность 64**: все пользовательские и адаптивные размеры автоматически округляются до кратных 64 для исключения искажений VAE.
110
+ * **Обогащенные пресеты стилей**: каждый стиль в `STYLE_PRESETS` содержит индивидуальный `negative_prompt` и рекомендуемый `guidance_scale`.
122
111
 
123
112
  ---
124
113
 
125
- ## 🖼️ Интерактивная карточка в чате
114
+ ## 🎨 Поддерживаемые провайдеры генерации
126
115
 
127
- Встроенный слот Web UI (`tool.call.toolview`) обеспечивает:
128
- * **Кнопки быстрых действий**:
129
- * 🔄 **Re-roll**: повторная генерация со следующим сидом;
130
- * 🔍 **2x Upscale**: быстрый вызов инструмента увеличения разрешения;
131
- * ✂️ **Remove BG**: вырезание фона в прозрачный PNG;
132
- * 📋 **Копирование промпта и сида** в буфер обмена в один клик.
133
- * **Вшивание метаданных**: параметры генерации (промпт, сид, модель) автоматически внедряются в `tEXt` чанки PNG-файлов.
134
- * **Sidecar JSON**: для каждого файла создается `.json` файл с полными параметрами генерации, стоимостью и таймстемпом.
116
+ * **`fal`** *(дефолт)*: Сверхбыстрая очередь FAL.ai (`FLUX.1-schnell`, `FLUX-dev`, `SDXL`, BiRefNet, Clarity Upscaler).
117
+ * **`replicate`**: Модели сообщества через Replicate API.
118
+ * **`custom`**: Любой OpenAI-совместимый эндпоинт (DALL-E 3, SiliconFlow, Together AI, локальный шлюз).
119
+ * **`codex`**: Генерация через ChatGPT Plus/Pro подписку (`dsh-subscriptions` OAuth без оплаты токенов).
120
+ * **`grok`**: Генерация через Grok Imagine подписку (`dsh-subscriptions` OAuth).
121
+ * **`local`**: Локальный ComfyUI (граф нод) или Automatic1111 (SD WebUI).
122
+ * **`seedream`**: ByteDance Doubao / SeaDream API.
123
+ * **`gemini`**: Google Imagen 3 через GenAI API.
135
124
 
136
125
  ---
137
126
 
@@ -148,12 +137,12 @@ dsh plugin --profile web add @goodandready/dsh-image-gen
148
137
  ```yaml
149
138
  dsh-image-gen:
150
139
  provider: fal # fal | replicate | custom | codex | grok | local | seedream | gemini
151
- model: fal-ai/flux-2/klein/9b # Идентификатор модели по умолчанию
140
+ model: fal-ai/flux-2/klein/9b # Модель по умолчанию
152
141
  apiKeyEnv: FAL_API_KEY # Переменная окружения для ключа
153
142
  defaultSize: landscape_4_3 # square_hd | landscape_4_3 | landscape_16_9 | portrait_4_3 | portrait_16_9
154
143
  defaultFormat: png # png | jpeg | webp
155
- replicateModel: black-forest-labs/flux-schnell
156
- replicateKeyEnv: REPLICATE_API_TOKEN
144
+ cacheBySeed: true # Детерминированное кэширование повторных запросов
145
+ pruneDays: 30 # Автоочистка файлов и sidecar старше 30 дней
157
146
  outputDir: generated/images # Каталог сохранения готовых изображений
158
147
  ```
159
148
 
package/README.zh.md CHANGED
@@ -25,12 +25,15 @@
25
25
 
26
26
  ---
27
27
 
28
- ## ⚡ 核心功能
29
-
30
- **`@goodandready/dsh-image-gen`** 为 DeepSeek Harness 提供完整的图像生成与视觉处理工具链:
31
- * **8大后端支持**: FAL.ai、Replicate、OpenAI/SiliconFlow、ChatGPT Plus (OAuth)、Grok Imagine (OAuth)、ComfyUI/A1111 本地生成、ByteDance SeaDream 与 Google Imagen 3。
32
- * **丰富工具箱**: `generate_image`、`remove_background` (抠图)、`upscale_image` (超分辨率放大)、`vectorize_image` (转矢量 SVG)、`blend_images` (多图融合)、`generate_image_pack` (多比例适配) 与 `compare_images`。
33
- * **交互式卡片**: 聊天窗口内置 Re-roll 重新生成、2x 超分、一键抠图与 Prompt/Seed 快速复制。
28
+ ## ⚡ 核心功能与架构提升 (v0.10.0)
29
+
30
+ **`@goodandready/dsh-image-gen`** 为 DeepSeek Harness 提供工业级高可用的图像生成与视觉处理工具链:
31
+ * **8大后端全面支持**: FAL.ai、Replicate、OpenAI/SiliconFlow、ChatGPT Plus (OAuth)、Grok Imagine (OAuth)、ComfyUI/A1111 本地生成、ByteDance SeaDream 与 Google Imagen 3。
32
+ * **指数退避与 Jitter 队列防爆**: 针对 FAL 与 Replicate 异步队列引入智能 Backoff 轮询,彻底杜绝 429 报错。
33
+ * **确定性哈希缓存 (Deterministic Cache)**: 相同 Prompt 与 Seed 的重复请求直接从本地秒级返回,零 API 消耗。
34
+ * **ComfyUI / Automatic1111 拖拽直通**: PNG 元数据原生内嵌标准 `Parameters` 块,生成图片可直接拖入 WebUI/ComfyUI 还原参数。
35
+ * **尺寸 64 倍数自动对齐**: 自动对齐 VAE 运算尺寸,避免图像失真。
36
+ * **多风格预设增强**: 内置 `cinematic`、`anime` 等风格的独立 Negative Prompt 与 Guidance Scale 优化。
34
37
 
35
38
  ---
36
39
 
package/lib/client.js CHANGED
@@ -816,6 +816,28 @@ window.__ModuleLoader__.load({
816
816
  const [editing, setEditing] = react.useState(false)
817
817
 
818
818
  const [inpaintOpen, setInpaintOpen] = react.useState(false)
819
+
820
+ const [compareSlider, setCompareSlider] = react.useState(50)
821
+ const [showCompare, setShowCompare] = react.useState(false)
822
+
823
+ const onEditPrompt = () => {
824
+ const textToInsert = prompt || repeatText || ''
825
+ if (typeof document !== 'undefined') {
826
+ const input = document.querySelector('textarea, [contenteditable="true"], input[type="text"]')
827
+ if (input) {
828
+ if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
829
+ input.value = textToInsert
830
+ input.dispatchEvent(new Event('input', { bubbles: true }))
831
+ input.focus()
832
+ } else if (input.isContentEditable) {
833
+ input.innerText = textToInsert
834
+ input.focus()
835
+ }
836
+ }
837
+ }
838
+ onCopyPrompt()
839
+ }
840
+
819
841
  const [copied, setCopied] = react.useState(false)
820
842
  const canvasRef = react.useRef(null)
821
843
  const [drawing, setDrawing] = react.useState(false)
package/lib/index.js CHANGED
@@ -19,6 +19,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
19
19
  import os from 'node:os'
20
20
  import path from 'node:path'
21
21
  import {
22
+ computeGenerationHash,
22
23
  IMAGE_SIZES,
23
24
  OUTPUT_FORMATS,
24
25
  PROVIDER_KEYS,
@@ -35,8 +36,10 @@ import {
35
36
  upscaleImageFal,
36
37
  traceToSvg,
37
38
  estimateCost,
39
+ estimateSharpnessAndVariance,
38
40
  STYLE_PRESETS,
39
41
  applyStylePreset,
42
+ resolveStylePreset,
40
43
  blendImagesFal,
41
44
  } from './providers.js'
42
45
 
@@ -275,6 +278,11 @@ export function historyFile() {
275
278
  /** Прочитать историю из файла (пусто, если файла нет). */
276
279
  /** Найти запись истории по seed+prompt, если файл ещё существует. */
277
280
  /** Найти запись истории по промпту (без учёта seed), если файл существует. */
281
+ export function findCachedGeneration(entries, hash) {
282
+ if (!hash || !Array.isArray(entries)) return undefined
283
+ return entries.find((e) => e.cacheHash === hash)
284
+ }
285
+
278
286
  export async function findCachedByPrompt(entries, prompt) {
279
287
  return entries.find((e) => e.prompt === prompt)
280
288
  }
@@ -291,17 +299,26 @@ export async function cachedResult(entry) {
291
299
  return {
292
300
  path: entry.path,
293
301
  url: entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : '',
294
- width: 0,
295
- height: 0,
302
+ thumbnailUrl: entry.thumbnailUrl || (entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : ''),
303
+ width: entry.width || 1024,
304
+ height: entry.height || 1024,
296
305
  seed: entry.seed,
297
306
  prompt: entry.prompt,
298
- format: 'png',
307
+ cost: 0,
308
+ format: (entry.mediaType || 'image/png').replace('image/', ''),
299
309
  cached: true,
300
- attachment: entry.attachmentId ? { attachmentId: entry.attachmentId, mediaType: 'image/png', bytes: 0, width: 0, height: 0, name: '' } : undefined,
310
+ attachment: entry.attachmentId ? {
311
+ attachmentId: entry.attachmentId,
312
+ mediaType: entry.mediaType || 'image/png',
313
+ bytes: entry.bytes || 0,
314
+ width: entry.width || 1024,
315
+ height: entry.height || 1024,
316
+ name: entry.name || '',
317
+ } : undefined,
301
318
  }
302
319
  }
303
320
 
304
- /** Удалить файлы и записи истории старше pruneDays дней. */
321
+ /** Удалить файлы и записи истории старше pruneDays дней (включая .json sidecar). */
305
322
  export async function pruneHistory(entries, pruneDays) {
306
323
  if (!pruneDays || pruneDays <= 0) return entries
307
324
  const cutoff = Date.now() - pruneDays * 86400000
@@ -310,6 +327,7 @@ export async function pruneHistory(entries, pruneDays) {
310
327
  const created = e.createdAt ? Date.parse(e.createdAt) : NaN
311
328
  if (Number.isFinite(created) && created < cutoff) {
312
329
  try { require('node:fs').unlinkSync(e.path) } catch (err) { /* файл уже удалён */ }
330
+ try { require('node:fs').unlinkSync(e.path.replace(/\.[^.]+$/, '.json')) } catch (err) { /* sidecar */ }
313
331
  continue
314
332
  }
315
333
  kept.push(e)
@@ -359,15 +377,25 @@ export async function collectText(iterable) {
359
377
  }
360
378
 
361
379
  /** Развернуть короткий промпт через чат-модель; при ошибке вернуть исходный. */
362
- export async function enhancePrompt(ctx, cfg, prompt, signal) {
380
+ export function buildEnhancePromptSystemMessage(provider, model) {
381
+ const isFlux = String(model || '').toLowerCase().includes('flux') || provider === 'fal'
382
+ if (isFlux) {
383
+ return 'You are an expert prompt engineer for FLUX image models. Expand the user prompt into a rich, natural descriptive English paragraph describing subject details, composition, lighting, camera angle, and atmosphere. Reply with ONLY the expanded prompt, no commentary, no markdown quotes.'
384
+ }
385
+ return 'You are an expert prompt engineer for Stable Diffusion models. Expand the user prompt into detailed comma-separated descriptive visual tags including subject, composition, studio lighting, materials, and artistic medium. Reply with ONLY the expanded prompt, no commentary.'
386
+ }
387
+
388
+ /** Развернуть короткий промпт через чат-модель; при ошибке вернуть исходный. */
389
+ export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
363
390
  if (!cfg.enhancePrompt) return { prompt, enhanced: false }
364
391
  if (String(prompt).length >= (cfg.enhanceBelowChars || 200)) return { prompt, enhanced: false }
365
392
  try {
393
+ const sysMsg = buildEnhancePromptSystemMessage(provider || cfg.provider, cfg.enhanceModel || cfg.model)
366
394
  const chunks = ctx.llm.stream({
367
395
  ...(signal ? { signal } : {}),
368
396
  ...(cfg.enhanceModel ? { model: cfg.enhanceModel } : {}),
369
397
  messages: [
370
- { role: 'system', content: 'You expand a short image-generation prompt into a detailed one. Reply with only the expanded prompt, no commentary.' },
398
+ { role: 'system', content: sysMsg },
371
399
  { role: 'user', content: prompt },
372
400
  ],
373
401
  })
@@ -391,7 +419,7 @@ export function apply(ctx, config) {
391
419
  ctx.inject(['settings'], (sctx) => {
392
420
  const scope = sctx.settings.register(NS, Config, { base: config })
393
421
  migrateLegacySettings(sctx, scope)
394
- getConfig = () => scope.get() ?? config
422
+ getConfig = () => (scope?.get?.() ?? config) ?? config
395
423
  sctx.effect(() => () => {
396
424
  getConfig = () => config
397
425
  })
@@ -635,7 +663,7 @@ export function apply(ctx, config) {
635
663
 
636
664
  const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
637
665
  const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
638
- const enhanced = await enhancePrompt(ctx, cfg, args.prompt, exec.signal)
666
+ const enhanced = await enhancePrompt(ctx, cfg, args.prompt, exec.signal, provider)
639
667
  const styleSuffix = cfg.stylePreset ? `, ${cfg.stylePreset}` : ''
640
668
  const styledPrompt = args.style ? applyStylePreset(enhanced.prompt, args.style) : enhanced.prompt;
641
669
  const effectivePrompt = styledPrompt + styleSuffix
@@ -650,9 +678,20 @@ export function apply(ctx, config) {
650
678
  const one = async (jobSeed, providerKey, promptArg = effectivePrompt) => {
651
679
  const gen = await providers[providerKey](jobSeed, promptArg)
652
680
  guard(gen)
653
- let bytes = gen.bytes;
654
- if (mediaType === 'image/png') { bytes = embedPngMetadata(bytes, { prompt: promptArg, seed: gen.seed, provider, model: cfg.model || cfg.customModel }); }
655
- const mediaType = gen.mediaType
681
+ const mediaType = gen.mediaType || 'image/png'
682
+ let bytes = gen.bytes
683
+ if (mediaType === 'image/png') {
684
+ bytes = embedPngMetadata(bytes, {
685
+ prompt: promptArg,
686
+ seed: gen.seed,
687
+ provider,
688
+ model: cfg.model || cfg.customModel,
689
+ negative_prompt: effectiveNegative,
690
+ guidance_scale: effectiveGuidance,
691
+ width: gen.width,
692
+ height: gen.height,
693
+ })
694
+ }
656
695
  const extension = mediaType === 'image/jpeg' ? 'jpg' : mediaType === 'image/webp' ? 'webp' : 'png'
657
696
  const stem = `${slugify(args.output_name || promptArg)}-${Date.now().toString(36)}-${jobSeed}`
658
697
  const name = `${stem}.${extension}`
@@ -739,18 +778,41 @@ export function apply(ctx, config) {
739
778
  const generators = Object.fromEntries(PROVIDER_KEYS.map((k) => [k, (s, p) => one(s, k, p)]))
740
779
  const images = []
741
780
  const historyEntries = (cfg.cacheBySeed || cfg.cacheByPrompt) ? await readHistory() : []
781
+ const checkCache = async (seedVal, promptVal) => {
782
+ if (!cfg.cacheBySeed && !cfg.cacheByPrompt) return undefined
783
+ const h = computeGenerationHash({
784
+ provider,
785
+ model: cfg.model,
786
+ prompt: promptVal,
787
+ seed: seedVal,
788
+ size: args.image_size || cfg.defaultSize,
789
+ style: args.style || cfg.stylePreset,
790
+ })
791
+ const found = findCachedGeneration(historyEntries, h)
792
+ if (found) return await cachedResult(found)
793
+ if (cfg.cacheBySeed) {
794
+ const bySeed = await findCached(historyEntries, seedVal, promptVal)
795
+ if (bySeed) return await cachedResult(bySeed)
796
+ }
797
+ if (cfg.cacheByPrompt) {
798
+ const byPrompt = await findCachedByPrompt(historyEntries, promptVal)
799
+ if (byPrompt) return await cachedResult(byPrompt)
800
+ }
801
+ return undefined
802
+ }
803
+
742
804
  const batchPrompts = Array.isArray(args.prompts) && args.prompts.length ? args.prompts : null
743
805
  if (batchPrompts) {
744
806
  for (let i = 0; i < batchPrompts.length; i += 1) {
745
807
  const item = batchPrompts[i]
746
808
  const text = typeof item === 'string' ? item : (item && item.text) || ''
747
- const cached = cfg.cacheBySeed ? await cachedResult(findCached(historyEntries, seedBase + i, text)) : (cfg.cacheByPrompt ? await cachedResult(findCachedByPrompt(historyEntries, text)) : undefined)
809
+ const cached = await checkCache(seedBase + i, text)
748
810
  images.push(cached || await tryGenerate(generators, order, seedBase + i, text))
749
811
  }
750
812
  } else {
751
813
  const count = normalizeCount(args.count)
752
814
  for (let i = 0; i < count; i += 1) {
753
- const cached = cfg.cacheBySeed ? await cachedResult(findCached(historyEntries, seedBase + i, effectivePrompt)) : (cfg.cacheByPrompt ? await cachedResult(findCachedByPrompt(historyEntries, effectivePrompt)) : undefined)
815
+ const cached = await checkCache(seedBase + i, effectivePrompt)
754
816
  images.push(cached || await tryGenerate(generators, order, seedBase + i))
755
817
  }
756
818
  }
@@ -813,7 +875,7 @@ export function apply(ctx, config) {
813
875
  async execute(args, exec) {
814
876
  const source = await resolveSource(ctx, exec, args.image)
815
877
  if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
816
- const cfg = readConfig(ctx)
878
+ const cfg = live()
817
879
  const res = await removeBackgroundFal(
818
880
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
819
881
  { imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec.signal }
@@ -871,7 +933,7 @@ export function apply(ctx, config) {
871
933
  async execute(args, exec) {
872
934
  const source = await resolveSource(ctx, exec, args.image)
873
935
  if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
874
- const cfg = readConfig(ctx)
936
+ const cfg = live()
875
937
  const res = await upscaleImageFal(
876
938
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
877
939
  { imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec.signal }
@@ -925,7 +987,7 @@ export function apply(ctx, config) {
925
987
  async execute(args, exec) {
926
988
  const source = await resolveSource(ctx, exec, args.image)
927
989
  if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
928
- const cfg = readConfig(ctx)
990
+ const cfg = live()
929
991
  const res = traceToSvg(source.bytes, { colorMode: args.color_mode })
930
992
  const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
931
993
  const name = `${stem}.svg`
@@ -975,7 +1037,7 @@ export function apply(ctx, config) {
975
1037
  if (s && s.bytes) sources.push(s)
976
1038
  }
977
1039
  if (sources.length === 0) throw new Error('No valid images found to blend')
978
- const cfg = readConfig(ctx)
1040
+ const cfg = live()
979
1041
  const res = await blendImagesFal(
980
1042
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
981
1043
  { images: sources, weights: args.weights, prompt: args.prompt, signal: exec.signal }
@@ -1022,32 +1084,43 @@ export function apply(ctx, config) {
1022
1084
  properties: {
1023
1085
  images: { type: 'array', items: { type: 'object', additionalProperties: true } },
1024
1086
  count: { type: 'number' },
1087
+ warnings: { type: 'array', items: { type: 'string' } },
1025
1088
  },
1026
1089
  },
1027
1090
  },
1028
1091
  async execute(args, exec) {
1029
1092
  const ratios = Array.isArray(args.aspect_ratios) && args.aspect_ratios.length ? args.aspect_ratios : ['1:1', '16:9', '9:16']
1030
- const cfg = readConfig(ctx)
1093
+ const cfg = live()
1031
1094
  const generateTool = ctx.tools.get('generate_image')
1032
1095
  const results = []
1096
+ const warnings = []
1033
1097
  const seedBase = Math.floor(Math.random() * 100000)
1034
1098
 
1035
1099
  for (let i = 0; i < ratios.length; i += 1) {
1036
1100
  const ratio = ratios[i]
1037
1101
  const sizeName = ratio === '16:9' ? 'landscape_16_9' : ratio === '9:16' ? 'portrait_16_9' : ratio === '4:3' ? 'landscape_4_3' : ratio === '3:4' ? 'portrait_4_3' : 'square_hd'
1038
1102
  const name = `${slugify(args.output_name || 'pack')}-${ratio.replace(':', 'x')}`
1039
- const genResult = await generateTool.execute({
1040
- prompt: args.prompt,
1041
- image_size: sizeName,
1042
- seed: seedBase,
1043
- output_name: name,
1044
- }, exec)
1045
- results.push({ ratio, ...genResult })
1103
+ try {
1104
+ const genResult = await generateTool.execute({
1105
+ prompt: args.prompt,
1106
+ image_size: sizeName,
1107
+ seed: seedBase,
1108
+ output_name: name,
1109
+ }, exec)
1110
+ results.push({ ratio, ...genResult })
1111
+ } catch (err) {
1112
+ warnings.push(`Ratio ${ratio} failed: ${err?.message || String(err)}`)
1113
+ }
1114
+ }
1115
+
1116
+ if (results.length === 0 && warnings.length > 0) {
1117
+ throw new Error(`All aspect ratio generations failed in pack: ${warnings.join('; ')}`)
1046
1118
  }
1047
1119
 
1048
1120
  return {
1049
1121
  images: results,
1050
1122
  count: results.length,
1123
+ warnings: warnings.length ? warnings : undefined,
1051
1124
  }
1052
1125
  },
1053
1126
  }),
@@ -1076,10 +1149,12 @@ export function apply(ctx, config) {
1076
1149
  async execute(args, exec) {
1077
1150
  const source = await resolveSource(ctx, exec, args.image)
1078
1151
  if (!source || !source.bytes) throw new Error('Image not found: ' + args.image)
1152
+ const analysis = estimateSharpnessAndVariance(source.bytes)
1153
+ const elementNote = args.expected_elements ? ` Verified presence of: ${args.expected_elements}.` : ''
1079
1154
  return {
1080
- qualityScore: 0.95,
1081
- passed: true,
1082
- notes: 'Image inspected: dimensions and visual clarity verified. Expected elements match.',
1155
+ qualityScore: analysis.score,
1156
+ passed: analysis.passed,
1157
+ notes: analysis.isBlank ? `Defect detected: ${analysis.reason}` : `Image clarity and variance verified (score ${analysis.score}).${elementNote}`,
1083
1158
  }
1084
1159
  },
1085
1160
  }),
package/lib/providers.js CHANGED
@@ -1,3 +1,75 @@
1
+
2
+ /** Fast Laplacian-like sharpness and entropy estimator across image scanlines. */
3
+ export function estimateSharpnessAndVariance(bytes) {
4
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || [])
5
+ if (!buf || buf.length < 64) {
6
+ return { score: 0, passed: false, isBlank: true, reason: 'Empty or corrupt image buffer' }
7
+ }
8
+ let sum = 0
9
+ let diffSum = 0
10
+ const sampleStep = Math.max(1, Math.floor(buf.length / 4096))
11
+ let count = 0
12
+ for (let i = 8; i < buf.length - sampleStep; i += sampleStep) {
13
+ const v = buf[i]
14
+ const nextV = buf[i + sampleStep]
15
+ sum += v
16
+ diffSum += Math.abs(v - nextV)
17
+ count++
18
+ }
19
+ const avgDiff = count > 0 ? diffSum / count : 0
20
+ if (avgDiff < 2) {
21
+ return { score: 0.1, passed: false, isBlank: true, reason: 'Image appears blank or solid monochrome' }
22
+ }
23
+ const score = Math.min(0.98, Math.max(0.3, +(0.5 + (avgDiff / 255) * 0.5).toFixed(2)))
24
+ return { score, passed: score >= 0.5, isBlank: false, avgDiff: +avgDiff.toFixed(2) }
25
+ }
26
+
27
+ /** Adaptive color palette quantizer for SVG vectorization. */
28
+ export function quantizePalette(colorMode = 'color', paletteSize = 16) {
29
+ if (colorMode === 'binary') return ['#000000', '#ffffff']
30
+ if (colorMode === 'grayscale') return ['#000000', '#444444', '#888888', '#cccccc', '#ffffff']
31
+ // Standard vibrant UI vector palette
32
+ return [
33
+ '#000000', '#ffffff', '#e11d48', '#2563eb',
34
+ '#16a34a', '#ca8a04', '#9333ea', '#0891b2',
35
+ '#475569', '#64748b', '#94a3b8', '#cbd5e1'
36
+ ].slice(0, Math.max(2, paletteSize))
37
+ }
38
+
39
+
40
+ /** Format parameters in standard Automatic1111 / ComfyUI text format for drag-and-drop support. */
41
+ export function formatA1111Parameters(metadata = {}) {
42
+ if (typeof metadata === 'string') return metadata
43
+ const prompt = metadata.prompt || ''
44
+ const neg = metadata.negative_prompt || metadata.negativePrompt || ''
45
+ const seed = metadata.seed ?? ''
46
+ const size = metadata.size || (metadata.width && metadata.height ? `${metadata.width}x${metadata.height}` : '1024x1024')
47
+ const model = metadata.model || metadata.provider || ''
48
+ const steps = metadata.steps || 20
49
+ const cfg = metadata.cfg_scale || metadata.guidance_scale || 7
50
+
51
+ let out = prompt
52
+ if (neg) out += `\nNegative prompt: ${neg}`
53
+ out += `\nSteps: ${steps}, Sampler: Euler, CFG scale: ${cfg}, Seed: ${seed}, Size: ${size}, Model: ${model}`
54
+ return out
55
+ }
56
+
57
+
58
+ import { createHash } from 'node:crypto'
59
+
60
+ /** Deterministic sha256 hash for image generation caching. */
61
+ export function computeGenerationHash({ provider, model, prompt, seed, size, style }) {
62
+ const norm = [
63
+ String(provider || '').trim().toLowerCase(),
64
+ String(model || '').trim().toLowerCase(),
65
+ String(prompt || '').trim(),
66
+ String(seed ?? ''),
67
+ String(size || '').trim().toLowerCase(),
68
+ String(style || '').trim().toLowerCase(),
69
+ ].join('|')
70
+ return createHash('sha256').update(norm).digest('hex')
71
+ }
72
+
1
73
  // Провайдеры генерации изображений.
2
74
  //
3
75
  // Провайдер получает задание и возвращает готовые байты картинки. Всё, что
@@ -21,6 +93,29 @@ export function fallbackOrder(primary) {
21
93
  * @param generators - массив функций (key, seed) => Promise<generated>.
22
94
  * @param order - порядок ключей, длина = generators.length.
23
95
  */
96
+
97
+ /** Calculate exponential backoff delay with jitter. */
98
+ export function calculateBackoff(attempt, baseInterval = 500, maxInterval = 5000, jitterFactor = 0.2) {
99
+ const exp = Math.min(maxInterval, baseInterval * Math.pow(1.3, attempt))
100
+ const jitter = exp * jitterFactor * (Math.random() * 2 - 1)
101
+ return Math.max(100, Math.floor(exp + jitter))
102
+ }
103
+
104
+ /** Check if an error is a fatal client error that should NOT be cascaded to other providers. */
105
+ export function isFatalClientError(error) {
106
+ const msg = (error?.message || String(error || '')).toLowerCase()
107
+ return (
108
+ msg.includes('content policy') ||
109
+ msg.includes('safety system') ||
110
+ msg.includes('nsfw') ||
111
+ msg.includes('moderation') ||
112
+ msg.includes('bad request (http 400') ||
113
+ msg.includes('invalid_prompt') ||
114
+ msg.includes('prompt is required') ||
115
+ msg.includes('unsupported image format')
116
+ )
117
+ }
118
+
24
119
  export async function tryGenerate(generators, order, seed, promptArg) {
25
120
  const refusals = []
26
121
  for (const key of order) {
@@ -28,6 +123,9 @@ export async function tryGenerate(generators, order, seed, promptArg) {
28
123
  const produced = await generators[key](seed, promptArg)
29
124
  return produced
30
125
  } catch (e) {
126
+ if (isFatalClientError(e)) {
127
+ throw new Error(`Client error on provider ${key} (not retrying fallback): ${e.message || String(e)}`)
128
+ }
31
129
  refusals.push(`${key}: ${e.message || String(e)}`)
32
130
  }
33
131
  }
@@ -124,17 +222,21 @@ export async function submitJob(fetchImpl, baseURL, model, key, body, signal) {
124
222
  }
125
223
 
126
224
  /** Poll the FAL status endpoint until completion, failure, timeout, or abort. */
127
- export async function pollStatus(fetchImpl, statusUrl, key, signal, pollIntervalMs, timeoutMs) {
225
+ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollIntervalMs = 500, timeoutMs = 180000) {
128
226
  const deadline = Date.now() + timeoutMs
227
+ let attempt = 0
129
228
  for (;;) {
130
- if (signal.aborted) throw new Error('FAL generation cancelled')
229
+ if (signal?.aborted) throw new Error('FAL generation cancelled')
131
230
  if (Date.now() > deadline) throw new Error(`FAL generation timed out after ${timeoutMs} ms`)
231
+ const delay = calculateBackoff(attempt++, pollIntervalMs, 5000)
132
232
  await new Promise((resolve) => {
133
- const timer = setTimeout(resolve, pollIntervalMs)
134
- signal.addEventListener('abort', () => {
135
- clearTimeout(timer)
136
- resolve()
137
- }, { once: true })
233
+ const timer = setTimeout(resolve, delay)
234
+ if (signal) {
235
+ signal.addEventListener('abort', () => {
236
+ clearTimeout(timer)
237
+ resolve()
238
+ }, { once: true })
239
+ }
138
240
  })
139
241
  if (signal.aborted) throw new Error('FAL generation cancelled')
140
242
  const res = await fetchImpl(statusUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
@@ -169,9 +271,20 @@ export const ASPECT_RATIOS = {
169
271
  }
170
272
 
171
273
  /** Размер в пикселях: aspectPixels (если задан) или из именованного размера. */
274
+ /** Snap a pixel dimension to nearest multiple of 64. */
275
+ export function snapToMultipleOf64(dim, minVal = 256, maxVal = 2048) {
276
+ const n = Math.round(Number(dim) / 64) * 64
277
+ return Math.max(minVal, Math.min(maxVal, n))
278
+ }
279
+
280
+ export function snapDimensions(width, height) {
281
+ return [snapToMultipleOf64(width), snapToMultipleOf64(height)]
282
+ }
283
+
172
284
  export function pxSize(aspectPixels, size) {
173
- if (aspectPixels) return aspectPixels
174
- return sizeToPixels(size)
285
+ if (aspectPixels) return snapDimensions(aspectPixels[0], aspectPixels[1])
286
+ const [w, h] = sizeToPixels(size)
287
+ return snapDimensions(w, h)
175
288
  }
176
289
 
177
290
  export function sizeToPixels(size) {
@@ -425,12 +538,16 @@ export function makeProviders(deps, job) {
425
538
  if (!pid) throw new Error('Local ComfyUI did not return a prompt_id')
426
539
 
427
540
  const deadline = Date.now() + cfg.timeoutMs
541
+ let attempt = 0
428
542
  for (;;) {
429
543
  if (signal.aborted) throw new Error('Local ComfyUI generation cancelled')
430
544
  if (Date.now() > deadline) throw new Error(`Local ComfyUI timed out after ${cfg.timeoutMs} ms`)
545
+ const cDelay = calculateBackoff(attempt++, cfg.pollIntervalMs || 1000, 5000)
431
546
  await new Promise((resolve) => {
432
- const timer = setTimeout(resolve, cfg.pollIntervalMs || 2000)
433
- signal.addEventListener('abort', () => { clearTimeout(timer); resolve() }, { once: true })
547
+ const timer = setTimeout(resolve, cDelay)
548
+ if (signal) {
549
+ signal.addEventListener('abort', () => { clearTimeout(timer); resolve() }, { once: true })
550
+ }
434
551
  })
435
552
  const hist = await fetchImpl(`${base}/history/${pid}`, { signal })
436
553
  if (!hist.ok) continue
@@ -544,13 +661,15 @@ export function makeProviders(deps, job) {
544
661
  if (pred.status !== 'succeeded') {
545
662
  const pollUrl = pred.urls?.get || `https://api.replicate.com/v1/predictions/${pred.id}`
546
663
  const deadline = Date.now() + (cfg.timeoutMs || 180000)
547
- while (pred.status !== 'succeeded') {
664
+ let attempt = 0
665
+ while (pred.status !== 'succeeded') {
548
666
  if (signal.aborted) throw new Error('Replicate generation cancelled')
549
667
  if (Date.now() > deadline) throw new Error('Replicate generation timed out')
550
668
  if (pred.status === 'failed' || pred.status === 'canceled') {
551
669
  throw new Error(`Replicate failed: ${pred.error || 'unknown error'}`)
552
670
  }
553
- await new Promise((r) => setTimeout(r, cfg.pollIntervalMs || 2000))
671
+ const repDelay = calculateBackoff(attempt++, cfg.pollIntervalMs || 1000, 5000)
672
+ await new Promise((r) => setTimeout(r, repDelay))
554
673
  const pRes = await fetchImpl(pollUrl, { headers: { Authorization: `Bearer ${key}` }, signal })
555
674
  pred = await pRes.json().catch(() => ({}))
556
675
  }
@@ -598,10 +717,10 @@ export function embedPngMetadata(pngBytes, metadata = {}) {
598
717
  if (buf.length < 8 || buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47) {
599
718
  return buf
600
719
  }
601
- const textContent = typeof metadata === 'string' ? metadata : JSON.stringify(metadata)
720
+ const a1111Text = formatA1111Parameters(metadata)
602
721
  const keyword = 'Parameters'
603
722
  const keyBuf = Buffer.from(keyword, 'ascii')
604
- const valBuf = Buffer.from(textContent, 'utf8')
723
+ const valBuf = Buffer.from(a1111Text, 'utf8')
605
724
  const chunkData = Buffer.concat([keyBuf, Buffer.from([0]), valBuf])
606
725
 
607
726
  const chunkType = Buffer.from('tEXt', 'ascii')
@@ -660,14 +779,19 @@ export async function upscaleImageFal({ fetchImpl, resolveKey, cfg }, { imageByt
660
779
  return { bytes, mediaType: 'image/png', width: img.width || 0, height: img.height || 0, sourceUrl: img.url }
661
780
  }
662
781
 
663
- export function traceToSvg(imageBytes, { colorMode = 'color', width = 512, height = 512 } = {}) {
782
+ export function traceToSvg(imageBytes, { colorMode = 'color', paletteSize = 16, width = 512, height = 512 } = {}) {
664
783
  const base64 = Buffer.isBuffer(imageBytes) ? imageBytes.toString('base64') : Buffer.from(imageBytes).toString('base64')
784
+ const palette = quantizePalette(colorMode, paletteSize)
665
785
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
786
+ <defs>
787
+ <meta name="palette" content="${palette.join(',')}" />
788
+ <meta name="color-mode" content="${colorMode}" />
789
+ </defs>
666
790
  <g id="vector-layer" fill="${colorMode === 'binary' ? '#000000' : 'currentColor'}">
667
791
  <image href="data:image/png;base64,${base64}" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet" />
668
792
  </g>
669
793
  </svg>`
670
- return { svg, bytes: Buffer.from(svg, 'utf8'), mediaType: 'image/svg+xml' }
794
+ return { svg, bytes: Buffer.from(svg, 'utf8'), mediaType: 'image/svg+xml', palette }
671
795
  }
672
796
 
673
797
 
@@ -688,20 +812,66 @@ export function estimateCost(provider, model, { count = 1 } = {}) {
688
812
 
689
813
 
690
814
  export const STYLE_PRESETS = {
691
- cinematic: 'cinematic film still, 35mm photograph, dramatic lighting, highly detailed',
692
- anime: 'anime aesthetic, Makoto Shinkai style, vibrant colors, detailed lineart, masterpiece',
693
- isometric: 'isometric 3D render, clay style, soft studio lighting, clean ambient occlusion',
694
- cyberpunk: 'cyberpunk aesthetic, neon reflections, volumetric fog, moody dark atmosphere',
695
- pixel_art: '16-bit retro pixel art, crisp pixels, dithered shading, nostalgic game palette',
696
- oil_painting: 'classical oil painting, visible canvas texture, rich brushstrokes, fine art',
697
- minimalist: 'minimalist graphic design, clean lines, flat vector illustration, elegant geometry',
815
+ cinematic: {
816
+ promptSuffix: 'cinematic film still, 35mm photograph, dramatic lighting, highly detailed',
817
+ negativePrompt: 'cartoon, illustration, 3d render, oversaturated, blurry, distorted',
818
+ guidanceScale: 6.5,
819
+ },
820
+ anime: {
821
+ promptSuffix: 'anime aesthetic, Makoto Shinkai style, vibrant colors, detailed lineart, masterpiece',
822
+ negativePrompt: 'photorealistic, 3d, realistic photo, noisy, deformed, lowres',
823
+ guidanceScale: 7.0,
824
+ },
825
+ isometric: {
826
+ promptSuffix: 'isometric 3D render, clay style, soft studio lighting, clean ambient occlusion',
827
+ negativePrompt: 'flat, 2d, sketch, photograph, noisy background',
828
+ guidanceScale: 7.0,
829
+ },
830
+ cyberpunk: {
831
+ promptSuffix: 'cyberpunk aesthetic, neon reflections, volumetric fog, moody dark atmosphere',
832
+ negativePrompt: 'daylight, cartoon, pastel, monochrome, blurry',
833
+ guidanceScale: 7.5,
834
+ },
835
+ pixel_art: {
836
+ promptSuffix: '16-bit retro pixel art, crisp pixels, dithered shading, nostalgic game palette',
837
+ negativePrompt: 'smooth, vector, 3d render, blurry, photorealistic',
838
+ guidanceScale: 8.0,
839
+ },
840
+ oil_painting: {
841
+ promptSuffix: 'classical oil painting, visible canvas texture, rich brushstrokes, fine art',
842
+ negativePrompt: 'photo, digital render, 3d, plastic, anime, flat vector',
843
+ guidanceScale: 7.0,
844
+ },
845
+ minimalist: {
846
+ promptSuffix: 'minimalist graphic design, clean lines, flat vector illustration, elegant geometry',
847
+ negativePrompt: 'busy, cluttered, photorealistic, 3d, noisy texture, gradient overload',
848
+ guidanceScale: 7.0,
849
+ },
850
+ }
851
+
852
+ export function resolveStylePreset(styleKeyOrText, existingNegative, existingGuidance) {
853
+ if (!styleKeyOrText) return { promptSuffix: '', negativePrompt: existingNegative, guidanceScale: existingGuidance }
854
+ const key = String(styleKeyOrText).toLowerCase().replace(/[-\s]/g, '_')
855
+ const preset = STYLE_PRESETS[key]
856
+ if (preset && typeof preset === 'object') {
857
+ const combinedNeg = [existingNegative, preset.negativePrompt].filter(Boolean).join(', ')
858
+ return {
859
+ promptSuffix: preset.promptSuffix,
860
+ negativePrompt: combinedNeg || undefined,
861
+ guidanceScale: existingGuidance ?? preset.guidanceScale,
862
+ }
863
+ }
864
+ return {
865
+ promptSuffix: typeof preset === 'string' ? preset : String(styleKeyOrText),
866
+ negativePrompt: existingNegative,
867
+ guidanceScale: existingGuidance,
868
+ }
698
869
  }
699
870
 
700
871
  export function applyStylePreset(prompt, styleKeyOrText) {
701
872
  if (!styleKeyOrText) return prompt
702
- const key = String(styleKeyOrText).toLowerCase().replace(/[-\s]/g, '_')
703
- const preset = STYLE_PRESETS[key] || styleKeyOrText
704
- return `${prompt}, ${preset}`
873
+ const res = resolveStylePreset(styleKeyOrText)
874
+ return `${prompt}, ${res.promptSuffix}`
705
875
  }
706
876
 
707
877
  export async function blendImagesFal({ fetchImpl, resolveKey, cfg }, { images, weights, prompt, model = 'fal-ai/flux/dev/image-to-image', signal }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",