@goodandready/dsh-image-gen 0.11.22 → 0.11.24

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,12 +1,12 @@
1
- import { sanitizeErrorAndLogs } from '../security.js'
2
- // theme-pair.js — generate_theme_pair tool (#189).
3
- // Synchronized dual generation of light and dark theme visuals with adaptive CSS snippets.
1
+ // theme-pair.js — generate_theme_pair tool (#189, #361).
2
+ // Generates complementary Light & Dark mode asset variants with synchronized compositions.
4
3
 
5
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
6
5
  import path from 'node:path'
7
6
  import { mkdir, writeFile } from 'node:fs/promises'
8
7
  import {
9
8
  ASPECT_RATIOS,
9
+ PROVIDER_KEYS,
10
10
  buildSidecar,
11
11
  makeProviders,
12
12
  toLosslessJson,
@@ -24,7 +24,7 @@ import {
24
24
  export function registerThemePairTools(ctx, deps) {
25
25
  const {
26
26
  live,
27
- slugify,
27
+ slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
28
28
  resolveApiKey,
29
29
  } = deps
30
30
 
@@ -33,23 +33,23 @@ export function registerThemePairTools(ctx, deps) {
33
33
  defineTool({
34
34
  name: 'generate_theme_pair',
35
35
  description:
36
- 'Generate synchronized dual illustrations or UI graphics designed specifically for light and dark application themes. '
37
- + 'Produces both light (#FFFFFF clean backdrop) and dark (#0F172A obsidian backdrop with luminous accents) variants with matched composition, plus ready-to-use HTML/CSS responsive markup.',
36
+ 'Generate matching Light and Dark mode UI illustration variants with synchronized seeds, '
37
+ + 'consistent composition, and inverted contrast palettes. Outputs a responsive <picture> tag with prefers-color-scheme.',
38
38
  parameters: {
39
39
  prompt: {
40
40
  type: 'string',
41
+ description: 'Core visual concept description (e.g. "cloud database topology architecture").',
41
42
  required: true,
42
- description: 'Core subject or illustration motif, e.g. "cloud server network topology" or "productivity dashboard banner".',
43
- },
44
- aspect_ratio: {
45
- type: 'string',
46
- enum: Object.keys(ASPECT_RATIOS),
47
- description: 'Aspect ratio for both variants: "1:1", "16:9", "4:3", etc. (default: "16:9").',
48
43
  },
49
44
  style: {
50
45
  type: 'string',
51
46
  enum: THEME_STYLES,
52
- description: 'Visual style aesthetic (default: "minimalist").',
47
+ description: 'Aesthetic visual direction: minimalist, neon_cyberpunk, editorial, gradient_glass, 3d_clay.',
48
+ },
49
+ aspect_ratio: {
50
+ type: 'string',
51
+ enum: ['16:9', '1:1', '4:3', '21:9'],
52
+ description: 'Asset aspect ratio (default: 16:9).',
53
53
  },
54
54
  dark_contrast_boost: {
55
55
  type: 'number',
@@ -63,6 +63,10 @@ export function registerThemePairTools(ctx, deps) {
63
63
  type: 'integer',
64
64
  description: 'Base random seed to synchronize composition between variants.',
65
65
  },
66
+ provider: {
67
+ type: 'string',
68
+ description: 'Optional provider override. Defaults to active provider.',
69
+ },
66
70
  },
67
71
  output: {
68
72
  schema: {
@@ -100,7 +104,7 @@ export function registerThemePairTools(ctx, deps) {
100
104
  },
101
105
  isConcurrencySafe: () => false,
102
106
  async execute(args, exec) {
103
- const cfg = live()
107
+ const cfg = typeof live === 'function' ? live() : (live?.value || {})
104
108
  if (cfg.enabled === false) {
105
109
  throw new Error('Image generation is disabled in settings.')
106
110
  }
@@ -121,7 +125,10 @@ export function registerThemePairTools(ctx, deps) {
121
125
  darkContrastBoost,
122
126
  })
123
127
 
124
- const primaryProvider = cfg.provider || 'fal'
128
+ const primaryProvider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
129
+ ? (args.provider || cfg.provider)
130
+ : (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
131
+
125
132
  const estimatedCost = calculateGenerationCost({
126
133
  provider: primaryProvider,
127
134
  model: cfg.model || cfg.customModel || 'default',
@@ -130,7 +137,7 @@ export function registerThemePairTools(ctx, deps) {
130
137
  })
131
138
  assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
132
139
 
133
- const chain = resolveFallbackChain(primaryProvider, cfg.fallbackChain)
140
+ const chain = resolveFallbackChain(primaryProvider, cfg.fallbackProviders, PROVIDER_KEYS)
134
141
  const baseSeed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 1000000)
135
142
 
136
143
  const sessionCwd = exec.agent?.session?.header?.cwd
@@ -141,6 +148,9 @@ export function registerThemePairTools(ctx, deps) {
141
148
  const slug = args.output_name ? slugify(args.output_name) : slugify(prompt).slice(0, 36)
142
149
  const ts = Date.now().toString(36)
143
150
 
151
+ let subscriptionImages
152
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
153
+
144
154
  // 1. Generate Light theme variant
145
155
  const lightJob = {
146
156
  prompt: lightPrompt,
@@ -150,22 +160,26 @@ export function registerThemePairTools(ctx, deps) {
150
160
  seed: baseSeed,
151
161
  signal: exec.signal,
152
162
  }
153
- const lightGen = await executeWithFallback({
163
+ const lightProviders = makeProviders(
164
+ { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
165
+ lightJob,
166
+ )
167
+ const lightGen = await executeWithFallback(
168
+ lightProviders,
154
169
  chain,
155
- liveConfig: cfg,
156
- makeProvidersFn: (deps, job) => makeProviders(deps, job),
157
- resolveApiKeyFn: resolveApiKey,
158
- job: lightJob,
159
- })
170
+ baseSeed,
171
+ lightPrompt,
172
+ { logger: ctx.logger },
173
+ )
160
174
 
161
175
  const lightFilename = `${slug}-light-${ts}.png`
162
176
  const lightPath = path.join(outDir, lightFilename)
163
- await writeFile(lightPath, lightGen.bytes)
177
+ await writeFile(lightPath, Buffer.from(lightGen.bytes))
164
178
 
165
- const lightAttachment = await saveAttachmentSafe(ctx, {
179
+ const { attachment: lightAttachment, localUrl: lightUrl } = await saveAttachmentSafe(ctx, {
166
180
  bytes: lightGen.bytes,
167
181
  mediaType: 'image/png',
168
- filename: lightFilename,
182
+ name: lightFilename,
169
183
  width: lightGen.width || aspectPixels[0],
170
184
  height: lightGen.height || aspectPixels[1],
171
185
  })
@@ -174,7 +188,6 @@ export function registerThemePairTools(ctx, deps) {
174
188
  ctx,
175
189
  meta: { provider: primaryProvider, model: cfg.model || cfg.customModel, prompt: lightPrompt },
176
190
  })
177
- const lightUrl = lightAttachment.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(lightAttachment.attachmentId)}` : ''
178
191
 
179
192
  // 2. Generate Dark theme variant (using matched seed)
180
193
  const darkJob = {
@@ -185,22 +198,32 @@ export function registerThemePairTools(ctx, deps) {
185
198
  seed: baseSeed,
186
199
  signal: exec.signal,
187
200
  }
188
- const darkGen = await executeWithFallback({
189
- chain,
190
- liveConfig: cfg,
191
- makeProvidersFn: (deps, job) => makeProviders(deps, job),
192
- resolveApiKeyFn: resolveApiKey,
193
- job: darkJob,
194
- })
201
+ const darkProviders = makeProviders(
202
+ { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
203
+ darkJob,
204
+ )
205
+
206
+ let darkGen
207
+ try {
208
+ darkGen = await executeWithFallback(
209
+ darkProviders,
210
+ chain,
211
+ baseSeed,
212
+ darkPrompt,
213
+ { logger: ctx.logger },
214
+ )
215
+ } catch (darkErr) {
216
+ throw new Error(`Failed to generate dark theme variant: ${darkErr.message}`)
217
+ }
195
218
 
196
219
  const darkFilename = `${slug}-dark-${ts}.png`
197
220
  const darkPath = path.join(outDir, darkFilename)
198
- await writeFile(darkPath, darkGen.bytes)
221
+ await writeFile(darkPath, Buffer.from(darkGen.bytes))
199
222
 
200
- const darkAttachment = await saveAttachmentSafe(ctx, {
223
+ const { attachment: darkAttachment, localUrl: darkUrl } = await saveAttachmentSafe(ctx, {
201
224
  bytes: darkGen.bytes,
202
225
  mediaType: 'image/png',
203
- filename: darkFilename,
226
+ name: darkFilename,
204
227
  width: darkGen.width || aspectPixels[0],
205
228
  height: darkGen.height || aspectPixels[1],
206
229
  })
@@ -208,7 +231,6 @@ export function registerThemePairTools(ctx, deps) {
208
231
  ctx,
209
232
  meta: { provider: primaryProvider, model: cfg.model || cfg.customModel, prompt: darkPrompt },
210
233
  })
211
- const darkUrl = darkAttachment.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(darkAttachment.attachmentId)}` : ''
212
234
 
213
235
  // Sidecars
214
236
  await writeFile(
@@ -220,8 +242,8 @@ export function registerThemePairTools(ctx, deps) {
220
242
  size: `${aspectPixels[0]}x${aspectPixels[1]}`,
221
243
  format: 'png',
222
244
  seed: baseSeed,
223
- light: { path: lightPath, url: lightUrl, attachmentId: lightAttachment.attachmentId },
224
- dark: { path: darkPath, url: darkUrl, attachmentId: darkAttachment.attachmentId },
245
+ light: { path: lightPath, url: lightUrl, attachmentId: lightAttachment?.attachmentId },
246
+ dark: { path: darkPath, url: darkUrl, attachmentId: darkAttachment?.attachmentId },
225
247
  cost: (lightGen.cost || 0) + (darkGen.cost || 0),
226
248
  }), null, 2),
227
249
  )
@@ -266,4 +288,4 @@ export function registerThemePairTools(ctx, deps) {
266
288
  }),
267
289
  )
268
290
  }, 'dsh-image-gen: tool generate_theme_pair')
269
- }
291
+ }
@@ -1,16 +1,18 @@
1
- import { sanitizeErrorAndLogs } from '../security.js'
2
- // vision-ocr.js — replace_image_text tool (#180).
3
- // Seamless text replacement and OCR localization via dsh-vision-bridge & inpainting.
1
+ // vision-ocr.js — replace_image_text tool (#180, #360).
2
+ // Grounding OCR, background inpainting, and typography re-rendering for visual localization.
4
3
 
5
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
6
5
  import path from 'node:path'
7
6
  import { mkdir, writeFile } from 'node:fs/promises'
8
7
  import {
9
- ASPECT_RATIOS,
8
+ PROVIDER_KEYS,
10
9
  makeProviders,
10
+ toLosslessJson,
11
11
  saveAttachmentSafe,
12
12
  } from '../providers.js'
13
+ import { buildDualOutputMarkdown } from '../resolve-image.js'
13
14
  import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
15
+ import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
14
16
  import { renderToolOutput } from '../attachment-helper.js'
15
17
  import {
16
18
  normalizeBbox,
@@ -22,7 +24,7 @@ export function registerVisionOcrTools(ctx, deps) {
22
24
  const {
23
25
  live,
24
26
  resolveSource,
25
- slugify,
27
+ slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
26
28
  resolveApiKey,
27
29
  } = deps
28
30
 
@@ -31,22 +33,28 @@ export function registerVisionOcrTools(ctx, deps) {
31
33
  defineTool({
32
34
  name: 'replace_image_text',
33
35
  description:
34
- 'Locate text inscriptions on an image via vision OCR, erase them seamlessly via inpainting, '
35
- + 'and render translated or replaced typography precisely at the original coordinates.',
36
+ 'Locate text elements in an image using OCR/vision grounding, inpaint the background to erase them, '
37
+ + 'and render crisp translated/replacement typography over the cleaned regions for visual localization.',
36
38
  parameters: {
37
39
  image: {
38
40
  type: 'string',
39
- description: 'Source image path, URL, or attachment id (sha256:...) containing text to replace.',
41
+ description: 'Source image path, attachment ID, or URL containing text to replace.',
40
42
  },
41
43
  replacements: {
42
44
  type: 'array',
43
- description: 'Array of text replacements: [{ find: "Old Text", replace: "New Text", bbox: [x1,y1,x2,y2] }].',
45
+ description: 'List of text replacement instructions.',
44
46
  items: {
45
47
  type: 'object',
46
48
  additionalProperties: true,
47
49
  properties: {
48
- find: { type: 'string', description: 'Original text to locate' },
49
- replace: { type: 'string', description: 'New replacement text to render' },
50
+ find: {
51
+ type: 'string',
52
+ description: 'Original text string to search for and erase.',
53
+ },
54
+ replace: {
55
+ type: 'string',
56
+ description: 'Replacement text to render in place of the original.',
57
+ },
50
58
  bbox: {
51
59
  type: 'array',
52
60
  items: { type: 'number' },
@@ -75,6 +83,10 @@ export function registerVisionOcrTools(ctx, deps) {
75
83
  type: 'string',
76
84
  description: 'Optional provider override for inpainting. Defaults to active provider.',
77
85
  },
86
+ seed: {
87
+ type: 'integer',
88
+ description: 'Optional seed for reproducible inpainting.',
89
+ },
78
90
  },
79
91
  output: {
80
92
  schema: {
@@ -90,6 +102,9 @@ export function registerVisionOcrTools(ctx, deps) {
90
102
  },
91
103
  },
92
104
  inpaintPrompt: { type: 'string' },
105
+ targetLanguage: { type: 'string' },
106
+ path: { type: 'string' },
107
+ url: { type: 'string' },
93
108
  attachment: {
94
109
  type: 'object',
95
110
  additionalProperties: true,
@@ -108,13 +123,23 @@ export function registerVisionOcrTools(ctx, deps) {
108
123
  type: 'object',
109
124
  additionalProperties: true,
110
125
  },
126
+ _fallback: {
127
+ type: 'object',
128
+ additionalProperties: true,
129
+ },
111
130
  },
112
131
  },
113
132
  render: (_args, value) => renderToolOutput(value),
114
133
  },
115
134
  async execute(args, execCtx) {
116
- const cfg = live?.value || (typeof live === 'function' ? live() : {})
117
- const activeProvider = args.provider || cfg.activeProvider || 'fal'
135
+ const cfg = typeof live === 'function' ? live() : (live?.value || {})
136
+ if (cfg.enabled === false) {
137
+ throw new Error('Image generation is disabled in settings.')
138
+ }
139
+
140
+ const provider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
141
+ ? (args.provider || cfg.provider)
142
+ : (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
118
143
 
119
144
  const source = await resolveSource(ctx, execCtx, args.image)
120
145
  if (!source || !source.bytes) {
@@ -148,7 +173,6 @@ export function registerVisionOcrTools(ctx, deps) {
148
173
  }
149
174
  }
150
175
 
151
- // Fallback default bounding box if detection didn't locate exact coordinates
152
176
  const normalizedBox = normalizeBbox(bbox || [200, 400, 800, 500], imgWidth, imgHeight)
153
177
  if (normalizedBox) {
154
178
  bboxes.push(normalizedBox)
@@ -160,12 +184,11 @@ export function registerVisionOcrTools(ctx, deps) {
160
184
  }
161
185
  }
162
186
 
163
- // If no replacements specified, check default center title box
164
187
  if (bboxes.length === 0) {
165
188
  const defaultBox = [150, 420, 850, 520]
166
189
  bboxes.push(defaultBox)
167
190
  overlayItems.push({
168
- text: 'Localized Text',
191
+ text: args.target_language ? `[${args.target_language}] Localized Text` : 'Localized Text',
169
192
  bbox: defaultBox,
170
193
  color: args.font_color || '#FFFFFF',
171
194
  })
@@ -181,12 +204,10 @@ export function registerVisionOcrTools(ctx, deps) {
181
204
 
182
205
  // 4. Inpaint to erase old text
183
206
  const inpaintPrompt = args.inpaint_prompt || 'clean seamless surface, erase text artifacts, coherent background texture'
184
- const providers = makeProviders({
185
- fetchImpl: fetch,
186
- resolveKey: (id) => resolveApiKey(cfg, id),
187
- cfg,
188
- })
189
- const fallbackChain = resolveFallbackChain(activeProvider, cfg)
207
+ const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
208
+
209
+ let subscriptionImages
210
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
190
211
 
191
212
  const inpaintJob = {
192
213
  prompt: inpaintPrompt,
@@ -200,77 +221,119 @@ export function registerVisionOcrTools(ctx, deps) {
200
221
  },
201
222
  strength: 0.95,
202
223
  format: 'png',
224
+ seed,
225
+ signal: execCtx?.signal,
226
+ }
227
+
228
+ const providers = makeProviders(
229
+ { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
230
+ inpaintJob,
231
+ )
232
+ const fallbackChain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
233
+
234
+ const estimatedCost = calculateGenerationCost({
235
+ provider,
236
+ model: cfg.model || cfg.customModel || 'default',
237
+ size: `${imgWidth}x${imgHeight}`,
238
+ count: 1,
239
+ })
240
+ assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
241
+
242
+ const gen = await executeWithFallback(
243
+ providers,
244
+ fallbackChain,
245
+ seed,
246
+ inpaintPrompt,
247
+ { logger: ctx.logger },
248
+ )
249
+
250
+ const inpaintedBytes = gen.bytes
251
+ if (!inpaintedBytes) {
252
+ throw new Error('Inpainting failed to return image bytes for text replacement')
203
253
  }
204
254
 
205
- let inpaintedBytes = null
206
- let inpaintedUrl = null
255
+ // 5. Composite typography overlay on inpainted background
256
+ let finalBytes = inpaintedBytes
207
257
  try {
208
- const { result: inpaintResult } = await executeWithFallback(
209
- fallbackChain,
210
- providers,
211
- inpaintJob,
212
- { signal: execCtx?.signal }
213
- )
214
- inpaintedBytes = inpaintResult.bytes
215
- inpaintedUrl = inpaintResult.url
216
- } catch (_err) {
217
- /* non-fatal: inpaint provider unavailable; use source bytes */
218
- inpaintedBytes = source.bytes
258
+ const sharpMod = await import('sharp')
259
+ const sharp = sharpMod.default || sharpMod
260
+ finalBytes = await sharp(inpaintedBytes)
261
+ .composite([{ input: overlayBuffer, top: 0, left: 0 }])
262
+ .png()
263
+ .toBuffer()
264
+ } catch (sharpErr) {
265
+ if (ctx.logger?.warn) ctx.logger.warn(`Sharp compositing skipped: ${sharpErr.message}`)
219
266
  }
220
267
 
221
- const outDir = path.resolve(process.cwd(), cfg.outputDir || 'output')
268
+ const usedProvider = gen._fallback?.providerUsed || provider
269
+ const sessionCwd = execCtx?.agent?.session?.header?.cwd
270
+ const targetDir = cfg.outputDir || 'generated/images'
271
+ const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
222
272
  await mkdir(outDir, { recursive: true })
223
273
  const timestamp = Date.now()
224
- const safeSlug = slugify(args.image.slice(0, 20)) || 'localized-image'
274
+ const safeSlug = slugify(String(args.image || '').slice(0, 20)) || 'localized-image'
225
275
 
226
- let attachment = null
227
- if (inpaintedBytes) {
228
- const filename = `text-replaced-${safeSlug}-${timestamp}.png`
229
- const diskPath = path.join(outDir, filename)
230
- await writeFile(diskPath, Buffer.from(inpaintedBytes))
231
- attachment = await saveAttachmentSafe(
232
- ctx,
233
- Buffer.from(inpaintedBytes),
234
- 'image/png',
235
- filename,
236
- diskPath
237
- )
238
- } else if (inpaintedUrl) {
239
- attachment = {
240
- url: inpaintedUrl,
241
- mediaType: 'image/png',
242
- filename: `text-replaced-${safeSlug}-${timestamp}.png`,
243
- }
244
- }
276
+ const filename = `text-replaced-${safeSlug}-${timestamp}.png`
277
+ const { attachment, localUrl } = await saveAttachmentSafe(ctx, {
278
+ bytes: finalBytes,
279
+ mediaType: 'image/png',
280
+ name: filename,
281
+ })
282
+
283
+ const diskPath = path.join(outDir, filename)
284
+ await writeFile(diskPath, Buffer.from(finalBytes))
245
285
 
246
286
  const maskFilename = `mask-text-${safeSlug}-${timestamp}.svg`
247
- const maskAttachment = await saveAttachmentSafe(
248
- ctx,
249
- maskBuffer,
250
- 'image/svg+xml',
251
- maskFilename,
252
- path.join(outDir, maskFilename)
253
- )
287
+ const { attachment: maskAttachment } = await saveAttachmentSafe(ctx, {
288
+ bytes: maskBuffer,
289
+ mediaType: 'image/svg+xml',
290
+ name: maskFilename,
291
+ })
254
292
 
255
293
  const overlayFilename = `overlay-text-${safeSlug}-${timestamp}.svg`
256
- const overlayAttachment = await saveAttachmentSafe(
294
+ const { attachment: overlayAttachment } = await saveAttachmentSafe(ctx, {
295
+ bytes: overlayBuffer,
296
+ mediaType: 'image/svg+xml',
297
+ name: overlayFilename,
298
+ })
299
+
300
+ const effectiveCost = Number(gen.cost) || estimatedCost
301
+ recordSpend(effectiveCost, {
257
302
  ctx,
258
- overlayBuffer,
259
- 'image/svg+xml',
260
- overlayFilename,
261
- path.join(outDir, overlayFilename)
262
- )
303
+ meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: inpaintPrompt, seed },
304
+ })
263
305
 
264
- return {
306
+ const dualOutput = buildDualOutputMarkdown({
307
+ action: 'generated',
308
+ filePath: diskPath,
309
+ width: imgWidth,
310
+ height: imgHeight,
311
+ mediaType: 'image/png',
312
+ seed,
313
+ provider: usedProvider,
314
+ model: cfg.model || cfg.customModel || 'replace-image-text',
315
+ cost: gen.cost,
316
+ attachmentId: attachment?.attachmentId,
317
+ })
318
+
319
+ const response = {
320
+ summary: dualOutput,
265
321
  replacedCount: overlayItems.length,
266
322
  bboxes,
267
323
  inpaintPrompt,
324
+ targetLanguage: args.target_language || '',
325
+ seed,
326
+ provider: usedProvider,
327
+ path: diskPath,
328
+ url: localUrl,
268
329
  attachment,
269
330
  maskAttachment,
270
331
  overlayAttachment,
271
- url: attachment?.url,
272
332
  attachmentId: attachment?.attachmentId,
333
+ _fallback: gen._fallback,
273
334
  }
335
+
336
+ return toLosslessJson(response)
274
337
  },
275
338
  })
276
339
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.22",
3
+ "version": "0.11.24",
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",