@goodandready/dsh-image-gen 0.11.31 → 0.11.32
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/lib/frontend-assets.js +57 -0
- package/lib/generation-helpers.js +28 -6
- package/lib/history.js +32 -0
- package/lib/provider-utils.js +5 -2
- package/lib/tools/character-sheet.js +7 -4
- package/lib/tools/diagram.js +7 -4
- package/lib/tools/generation.js +39 -9
- package/lib/tools/processing-basic.js +140 -107
- package/lib/tools/style-matrix.js +8 -1
- package/lib/tools/theme-pair.js +8 -1
- package/lib/tools/ui-asset.js +7 -4
- package/lib/tools/vision-ocr.js +7 -4
- package/package.json +2 -2
package/lib/frontend-assets.js
CHANGED
|
@@ -359,6 +359,61 @@ export function decodeHtmlEntities(str) {
|
|
|
359
359
|
.replace(/&/gi, '&')
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Escapes JSX-sensitive characters in SVG markup so text nodes and unquoted
|
|
364
|
+
* curly braces cannot be interpreted as JavaScript expressions in TSX export (#406).
|
|
365
|
+
* Replaces unquoted `{` and `}` outside attribute values with `{` and `}`.
|
|
366
|
+
* @param {string} content
|
|
367
|
+
* @returns {string}
|
|
368
|
+
*/
|
|
369
|
+
export function escapeJsxText(content) {
|
|
370
|
+
if (!content || typeof content !== 'string') return ''
|
|
371
|
+
let result = ''
|
|
372
|
+
let inTag = false
|
|
373
|
+
let quote = null
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < content.length; i++) {
|
|
376
|
+
const ch = content[i]
|
|
377
|
+
|
|
378
|
+
if (!inTag) {
|
|
379
|
+
if (ch === '<') {
|
|
380
|
+
inTag = true
|
|
381
|
+
quote = null
|
|
382
|
+
result += ch
|
|
383
|
+
} else if (ch === '{') {
|
|
384
|
+
result += '{'
|
|
385
|
+
} else if (ch === '}') {
|
|
386
|
+
result += '}'
|
|
387
|
+
} else {
|
|
388
|
+
result += ch
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
if (quote) {
|
|
392
|
+
if (ch === quote) {
|
|
393
|
+
quote = null
|
|
394
|
+
}
|
|
395
|
+
result += ch
|
|
396
|
+
} else {
|
|
397
|
+
if (ch === '"' || ch === "'") {
|
|
398
|
+
quote = ch
|
|
399
|
+
result += ch
|
|
400
|
+
} else if (ch === '>') {
|
|
401
|
+
inTag = false
|
|
402
|
+
result += ch
|
|
403
|
+
} else if (ch === '{') {
|
|
404
|
+
result += '{'
|
|
405
|
+
} else if (ch === '}') {
|
|
406
|
+
result += '}'
|
|
407
|
+
} else {
|
|
408
|
+
result += ch
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return result
|
|
415
|
+
}
|
|
416
|
+
|
|
362
417
|
/**
|
|
363
418
|
* Issue #176: optimize_vector_svg
|
|
364
419
|
* Cleans, sanitizes, and minifies SVG content, normalizes viewBox, and exports React TSX component.
|
|
@@ -463,6 +518,8 @@ export function optimizeSvgContent(svgString, options = {}) {
|
|
|
463
518
|
.replace(/stroke-linejoin=/g, 'strokeLinejoin=')
|
|
464
519
|
.replace(/stroke-miterlimit=/g, 'strokeMiterlimit=')
|
|
465
520
|
|
|
521
|
+
jsxBody = escapeJsxText(jsxBody)
|
|
522
|
+
|
|
466
523
|
const reactTsx = `import React from 'react';
|
|
467
524
|
|
|
468
525
|
export interface ${componentName}Props extends React.SVGProps<SVGSVGElement> {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Extracted execution and prompt/caching helpers for generate_image tool (#349).
|
|
3
3
|
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import {
|
|
7
8
|
computeGenerationHash,
|
|
@@ -153,6 +154,10 @@ export async function saveGeneratedImageArtifact({
|
|
|
153
154
|
estimatedCost,
|
|
154
155
|
totalCount,
|
|
155
156
|
recordSpend: shouldRecordSpend = true,
|
|
157
|
+
source,
|
|
158
|
+
mask,
|
|
159
|
+
resolvedFaceRef,
|
|
160
|
+
resolvedRefImage,
|
|
156
161
|
}) {
|
|
157
162
|
const targetFormat = format || (args.output_format || cfg.defaultFormat)
|
|
158
163
|
const formatted = await ensureImageFormat(gen.bytes, targetFormat, { fallbackMime: gen.mediaType })
|
|
@@ -205,7 +210,11 @@ export async function saveGeneratedImageArtifact({
|
|
|
205
210
|
|
|
206
211
|
const effectiveModel = provider === 'custom'
|
|
207
212
|
? (cfg.customModel || cfg.model || 'dall-e-3')
|
|
208
|
-
: (cfg.model || cfg.customModel)
|
|
213
|
+
: (cfg.model || cfg.customModel || 'default')
|
|
214
|
+
|
|
215
|
+
const sourceBytes = source && source.bytes ? source.bytes : (args.image || args.source_image)
|
|
216
|
+
const maskBytes = mask && mask.bytes ? mask.bytes : (args.mask_image || args.mask)
|
|
217
|
+
const refBytes = (resolvedRefImage && resolvedRefImage.bytes) || (resolvedFaceRef && resolvedFaceRef.bytes) || args.reference_images || args.reference_image
|
|
209
218
|
|
|
210
219
|
const cacheHash = computeGenerationHash({
|
|
211
220
|
provider,
|
|
@@ -222,9 +231,9 @@ export async function saveGeneratedImageArtifact({
|
|
|
222
231
|
negativePrompt: args.negative_prompt || cfg.negativePrompt,
|
|
223
232
|
quality: args.quality,
|
|
224
233
|
strength: args.strength,
|
|
225
|
-
sourceImage:
|
|
226
|
-
maskImage:
|
|
227
|
-
reference:
|
|
234
|
+
sourceImage: sourceBytes,
|
|
235
|
+
maskImage: maskBytes,
|
|
236
|
+
reference: refBytes,
|
|
228
237
|
})
|
|
229
238
|
|
|
230
239
|
// #170: Store in content-addressed disk cache
|
|
@@ -235,19 +244,32 @@ export async function saveGeneratedImageArtifact({
|
|
|
235
244
|
width: gen.width || attachment.width,
|
|
236
245
|
height: gen.height || attachment.height,
|
|
237
246
|
seed: gen.seed,
|
|
238
|
-
meta: { prompt: promptArg, provider, model:
|
|
247
|
+
meta: { prompt: promptArg, provider, model: effectiveModel, cost: gen.cost },
|
|
239
248
|
})
|
|
240
249
|
}
|
|
250
|
+
|
|
251
|
+
const sourceHash = (source && source.bytes) ? createHash('sha256').update(source.bytes).digest('hex') : undefined
|
|
252
|
+
const maskHash = (mask && mask.bytes) ? createHash('sha256').update(mask.bytes).digest('hex') : undefined
|
|
253
|
+
const refHash = (resolvedRefImage && resolvedRefImage.bytes)
|
|
254
|
+
? createHash('sha256').update(resolvedRefImage.bytes).digest('hex')
|
|
255
|
+
: ((resolvedFaceRef && resolvedFaceRef.bytes) ? createHash('sha256').update(resolvedFaceRef.bytes).digest('hex') : undefined)
|
|
256
|
+
|
|
241
257
|
const entry = {
|
|
242
258
|
path: filePath,
|
|
243
259
|
prompt: promptArg,
|
|
244
260
|
provider,
|
|
245
|
-
model:
|
|
261
|
+
model: effectiveModel,
|
|
246
262
|
size,
|
|
247
263
|
format,
|
|
248
264
|
aspectRatio: args.aspect_ratio || cfg.defaultAspectRatio,
|
|
249
265
|
seed: gen.seed,
|
|
250
266
|
cacheHash,
|
|
267
|
+
sourceHash,
|
|
268
|
+
maskHash,
|
|
269
|
+
refHash,
|
|
270
|
+
quality: args.quality,
|
|
271
|
+
style: args.style,
|
|
272
|
+
strength: args.strength,
|
|
251
273
|
thumbnailUrl: localUrl,
|
|
252
274
|
width: gen.width || attachment.width,
|
|
253
275
|
height: gen.height || attachment.height,
|
package/lib/history.js
CHANGED
|
@@ -41,6 +41,22 @@ export async function findCachedByPrompt(entries, prompt, criteria = {}) {
|
|
|
41
41
|
if (!prompt || !Array.isArray(entries)) return undefined
|
|
42
42
|
return entries.find((e) => {
|
|
43
43
|
if (e.prompt !== prompt) return false
|
|
44
|
+
if (criteria.cacheHash && e.cacheHash && criteria.cacheHash !== e.cacheHash) return false
|
|
45
|
+
if (criteria.provider && e.provider && criteria.provider.toLowerCase() !== e.provider.toLowerCase()) return false
|
|
46
|
+
if (criteria.model && e.model && criteria.model.toLowerCase() !== e.model.toLowerCase()) return false
|
|
47
|
+
if (criteria.sourceHash !== undefined && e.sourceHash !== undefined && criteria.sourceHash !== e.sourceHash) return false
|
|
48
|
+
if (criteria.sourceHash && !e.sourceHash) return false
|
|
49
|
+
if (!criteria.sourceHash && e.sourceHash) return false
|
|
50
|
+
if (criteria.maskHash !== undefined && e.maskHash !== undefined && criteria.maskHash !== e.maskHash) return false
|
|
51
|
+
if (criteria.maskHash && !e.maskHash) return false
|
|
52
|
+
if (!criteria.maskHash && e.maskHash) return false
|
|
53
|
+
if (criteria.refHash !== undefined && e.refHash !== undefined && criteria.refHash !== e.refHash) return false
|
|
54
|
+
if (criteria.refHash && !e.refHash) return false
|
|
55
|
+
if (!criteria.refHash && e.refHash) return false
|
|
56
|
+
if (criteria.quality !== undefined && e.quality !== undefined && criteria.quality !== e.quality) return false
|
|
57
|
+
if (criteria.style !== undefined && e.style !== undefined && criteria.style !== e.style) return false
|
|
58
|
+
if (criteria.strength !== undefined && e.strength !== undefined && criteria.strength !== e.strength) return false
|
|
59
|
+
|
|
44
60
|
if (criteria.format) {
|
|
45
61
|
const entryFmt = (e.format || (e.mediaType || '').replace('image/', '')).toLowerCase().replace('jpeg', 'jpg')
|
|
46
62
|
const targetFmt = String(criteria.format).toLowerCase().replace('jpeg', 'jpg')
|
|
@@ -58,6 +74,22 @@ export async function findCached(entries, seed, prompt, criteria = {}) {
|
|
|
58
74
|
if (seed === undefined || !Array.isArray(entries)) return undefined
|
|
59
75
|
return entries.find((e) => {
|
|
60
76
|
if (e.seed !== seed || e.prompt !== prompt) return false
|
|
77
|
+
if (criteria.cacheHash && e.cacheHash && criteria.cacheHash !== e.cacheHash) return false
|
|
78
|
+
if (criteria.provider && e.provider && criteria.provider.toLowerCase() !== e.provider.toLowerCase()) return false
|
|
79
|
+
if (criteria.model && e.model && criteria.model.toLowerCase() !== e.model.toLowerCase()) return false
|
|
80
|
+
if (criteria.sourceHash !== undefined && e.sourceHash !== undefined && criteria.sourceHash !== e.sourceHash) return false
|
|
81
|
+
if (criteria.sourceHash && !e.sourceHash) return false
|
|
82
|
+
if (!criteria.sourceHash && e.sourceHash) return false
|
|
83
|
+
if (criteria.maskHash !== undefined && e.maskHash !== undefined && criteria.maskHash !== e.maskHash) return false
|
|
84
|
+
if (criteria.maskHash && !e.maskHash) return false
|
|
85
|
+
if (!criteria.maskHash && e.maskHash) return false
|
|
86
|
+
if (criteria.refHash !== undefined && e.refHash !== undefined && criteria.refHash !== e.refHash) return false
|
|
87
|
+
if (criteria.refHash && !e.refHash) return false
|
|
88
|
+
if (!criteria.refHash && e.refHash) return false
|
|
89
|
+
if (criteria.quality !== undefined && e.quality !== undefined && criteria.quality !== e.quality) return false
|
|
90
|
+
if (criteria.style !== undefined && e.style !== undefined && criteria.style !== e.style) return false
|
|
91
|
+
if (criteria.strength !== undefined && e.strength !== undefined && criteria.strength !== e.strength) return false
|
|
92
|
+
|
|
61
93
|
if (criteria.format) {
|
|
62
94
|
const entryFmt = (e.format || (e.mediaType || '').replace('image/', '')).toLowerCase().replace('jpeg', 'jpg')
|
|
63
95
|
const targetFmt = String(criteria.format).toLowerCase().replace('jpeg', 'jpg')
|
package/lib/provider-utils.js
CHANGED
|
@@ -191,10 +191,13 @@ export function computeGenerationHash({
|
|
|
191
191
|
} = {}) {
|
|
192
192
|
const hashContent = (item) => {
|
|
193
193
|
if (!item) return ''
|
|
194
|
-
if (Buffer.isBuffer(item)) return createHash('sha256').update(item).digest('hex')
|
|
194
|
+
if (Buffer.isBuffer(item)) return createHash('sha256').update(item).digest('hex')
|
|
195
195
|
if (typeof item === 'object' && item.bytes) {
|
|
196
196
|
const b = Buffer.isBuffer(item.bytes) ? item.bytes : Buffer.from(item.bytes)
|
|
197
|
-
return createHash('sha256').update(b).digest('hex')
|
|
197
|
+
return createHash('sha256').update(b).digest('hex')
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(item)) {
|
|
200
|
+
return item.map(hashContent).filter(Boolean).join(',')
|
|
198
201
|
}
|
|
199
202
|
return String(item).trim()
|
|
200
203
|
}
|
|
@@ -154,6 +154,7 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
154
154
|
})
|
|
155
155
|
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
156
156
|
let gen
|
|
157
|
+
let committed = false
|
|
157
158
|
try {
|
|
158
159
|
gen = await executeWithFallback(
|
|
159
160
|
providers,
|
|
@@ -162,10 +163,6 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
162
163
|
sheetPlan.prompt,
|
|
163
164
|
{ logger: ctx.logger },
|
|
164
165
|
)
|
|
165
|
-
} catch (err) {
|
|
166
|
-
reservation.release()
|
|
167
|
-
throw err
|
|
168
|
-
}
|
|
169
166
|
|
|
170
167
|
const usedProvider = gen._fallback?.providerUsed || provider
|
|
171
168
|
const finalBytes = gen.bytes
|
|
@@ -205,6 +202,7 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
205
202
|
ctx,
|
|
206
203
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: sheetPlan.prompt, seed },
|
|
207
204
|
})
|
|
205
|
+
committed = true
|
|
208
206
|
|
|
209
207
|
const dualOutput = buildDualOutputMarkdown({
|
|
210
208
|
action: 'generated',
|
|
@@ -237,6 +235,11 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
237
235
|
}
|
|
238
236
|
|
|
239
237
|
return toLosslessJson(response)
|
|
238
|
+
} finally {
|
|
239
|
+
if (!committed) {
|
|
240
|
+
reservation.release()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
240
243
|
},
|
|
241
244
|
})
|
|
242
245
|
)
|
package/lib/tools/diagram.js
CHANGED
|
@@ -150,6 +150,7 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
150
150
|
})
|
|
151
151
|
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
152
152
|
let gen
|
|
153
|
+
let committed = false
|
|
153
154
|
try {
|
|
154
155
|
gen = await executeWithFallback(
|
|
155
156
|
providers,
|
|
@@ -158,10 +159,6 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
158
159
|
plan.prompt,
|
|
159
160
|
{ logger: ctx.logger },
|
|
160
161
|
)
|
|
161
|
-
} catch (err) {
|
|
162
|
-
reservation.release()
|
|
163
|
-
throw err
|
|
164
|
-
}
|
|
165
162
|
|
|
166
163
|
const usedProvider = gen._fallback?.providerUsed || provider
|
|
167
164
|
const finalBytes = gen.bytes
|
|
@@ -201,6 +198,7 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
201
198
|
ctx,
|
|
202
199
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: plan.prompt, seed },
|
|
203
200
|
})
|
|
201
|
+
committed = true
|
|
204
202
|
|
|
205
203
|
const dualOutput = buildDualOutputMarkdown({
|
|
206
204
|
action: 'generated',
|
|
@@ -232,6 +230,11 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
232
230
|
}
|
|
233
231
|
|
|
234
232
|
return toLosslessJson(response)
|
|
233
|
+
} finally {
|
|
234
|
+
if (!committed) {
|
|
235
|
+
reservation.release()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
235
238
|
},
|
|
236
239
|
})
|
|
237
240
|
)
|
package/lib/tools/generation.js
CHANGED
|
@@ -2,6 +2,7 @@ import { buildPresentationMeta } from '../attachment-helper.js'
|
|
|
2
2
|
import { registerGenerationPackTool } from './generation-pack.js'
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import {
|
|
7
8
|
computeGenerationHash,
|
|
@@ -316,8 +317,11 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
316
317
|
let subscriptionImages
|
|
317
318
|
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
318
319
|
|
|
319
|
-
const
|
|
320
|
-
const
|
|
320
|
+
const sourceInput = args.source_image || args.image
|
|
321
|
+
const source = sourceInput ? await resolveSource(ctx, exec, sourceInput) : undefined
|
|
322
|
+
|
|
323
|
+
const maskInput = args.mask || args.mask_image
|
|
324
|
+
const mask = maskInput ? await resolveSource(ctx, exec, maskInput) : undefined
|
|
321
325
|
|
|
322
326
|
const {
|
|
323
327
|
enhanced,
|
|
@@ -383,6 +387,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
383
387
|
const rawGenerate = async (seedVal) => {
|
|
384
388
|
const reservation = reserveSpend(perAttemptCost, cfg.dailyBudgetUsd)
|
|
385
389
|
totalAttempts++
|
|
390
|
+
let committed = false
|
|
386
391
|
try {
|
|
387
392
|
const gen = await providers[k](seedVal, p)
|
|
388
393
|
guard(gen)
|
|
@@ -392,10 +397,12 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
392
397
|
ctx,
|
|
393
398
|
meta: { provider: k, model: cfg.model || cfg.customModel, prompt: p, seed: seedVal },
|
|
394
399
|
})
|
|
400
|
+
committed = true
|
|
395
401
|
return gen
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
402
|
+
} finally {
|
|
403
|
+
if (!committed) {
|
|
404
|
+
reservation.release()
|
|
405
|
+
}
|
|
399
406
|
}
|
|
400
407
|
}
|
|
401
408
|
|
|
@@ -429,6 +436,10 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
429
436
|
estimatedCost: accumulatedCost || estimatedCost,
|
|
430
437
|
totalCount,
|
|
431
438
|
recordSpend: false,
|
|
439
|
+
source,
|
|
440
|
+
mask,
|
|
441
|
+
resolvedFaceRef,
|
|
442
|
+
resolvedRefImage,
|
|
432
443
|
})
|
|
433
444
|
|
|
434
445
|
return {
|
|
@@ -449,7 +460,11 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
449
460
|
|
|
450
461
|
const effectiveModel = provider === 'custom'
|
|
451
462
|
? (cfg.customModel || cfg.model || 'dall-e-3')
|
|
452
|
-
: (cfg.model || cfg.customModel)
|
|
463
|
+
: (cfg.model || cfg.customModel || 'default')
|
|
464
|
+
|
|
465
|
+
const sourceBytes = source && source.bytes ? source.bytes : (args.image || args.source_image)
|
|
466
|
+
const maskBytes = mask && mask.bytes ? mask.bytes : (args.mask_image || args.mask)
|
|
467
|
+
const refBytes = (resolvedRefImage && resolvedRefImage.bytes) || (resolvedFaceRef && resolvedFaceRef.bytes) || args.reference_images || args.reference_image
|
|
453
468
|
|
|
454
469
|
const h = computeGenerationHash({
|
|
455
470
|
provider,
|
|
@@ -466,9 +481,9 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
466
481
|
negativePrompt: args.negative_prompt || cfg.negativePrompt,
|
|
467
482
|
quality: args.quality,
|
|
468
483
|
strength: args.strength,
|
|
469
|
-
sourceImage:
|
|
470
|
-
maskImage:
|
|
471
|
-
reference:
|
|
484
|
+
sourceImage: sourceBytes,
|
|
485
|
+
maskImage: maskBytes,
|
|
486
|
+
reference: refBytes,
|
|
472
487
|
})
|
|
473
488
|
|
|
474
489
|
// #170: Content-addressed disk cache check (<50ms return)
|
|
@@ -512,10 +527,25 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
512
527
|
const hPrompt = h
|
|
513
528
|
const found = findCachedGeneration(historyEntries, hPrompt)
|
|
514
529
|
if (found) return await cachedResult(found)
|
|
530
|
+
const srcHash = (source && source.bytes) ? createHash('sha256').update(source.bytes).digest('hex') : undefined
|
|
531
|
+
const mskHash = (mask && mask.bytes) ? createHash('sha256').update(mask.bytes).digest('hex') : undefined
|
|
532
|
+
const rfHash = (resolvedRefImage && resolvedRefImage.bytes)
|
|
533
|
+
? createHash('sha256').update(resolvedRefImage.bytes).digest('hex')
|
|
534
|
+
: ((resolvedFaceRef && resolvedFaceRef.bytes) ? createHash('sha256').update(resolvedFaceRef.bytes).digest('hex') : undefined)
|
|
535
|
+
|
|
515
536
|
const requestedCriteria = {
|
|
537
|
+
provider,
|
|
538
|
+
model: effectiveModel,
|
|
539
|
+
cacheHash: h,
|
|
516
540
|
format: args.output_format || cfg.defaultFormat || 'png',
|
|
517
541
|
aspectRatio: args.aspect_ratio || cfg.defaultAspectRatio,
|
|
518
542
|
size: args.aspect_ratio ? 'custom' : (args.image_size || cfg.defaultSize),
|
|
543
|
+
quality: args.quality,
|
|
544
|
+
style: args.style,
|
|
545
|
+
strength: args.strength,
|
|
546
|
+
sourceHash: srcHash,
|
|
547
|
+
maskHash: mskHash,
|
|
548
|
+
refHash: rfHash,
|
|
519
549
|
}
|
|
520
550
|
if (cfg.cacheBySeed) {
|
|
521
551
|
const bySeed = await findCached(historyEntries, seedVal, promptVal, requestedCriteria)
|
|
@@ -56,38 +56,49 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
56
56
|
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
57
57
|
const cfg = live()
|
|
58
58
|
const resvNobg = reserveSpend(0.02, cfg.dailyBudgetUsd)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
filePath,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
59
|
+
let committed = false
|
|
60
|
+
try {
|
|
61
|
+
const res = await removeBackgroundFal(
|
|
62
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
63
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec?.signal }
|
|
64
|
+
)
|
|
65
|
+
if (!res || !res.bytes) {
|
|
66
|
+
throw new Error('remove_background provider returned empty or invalid response')
|
|
67
|
+
}
|
|
68
|
+
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
69
|
+
const name = `${stem}.png`
|
|
70
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
71
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
72
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
73
|
+
await mkdir(outDir, { recursive: true })
|
|
74
|
+
const filePath = path.join(outDir, name)
|
|
75
|
+
await writeFile(filePath, res.bytes)
|
|
76
|
+
resvNobg.commit(0.02, { ctx, meta: { tool: 'remove_background', model: args.model || 'fal-ai/birefnet' } })
|
|
77
|
+
committed = true
|
|
78
|
+
const summary = buildDualOutputMarkdown({
|
|
79
|
+
action: 'background removed',
|
|
80
|
+
filePath,
|
|
81
|
+
width: res.width || attachment.width,
|
|
82
|
+
height: res.height || attachment.height,
|
|
83
|
+
mediaType: 'image/png',
|
|
84
|
+
provider: 'fal',
|
|
85
|
+
model: args.model || 'fal-ai/birefnet',
|
|
86
|
+
attachmentId: attachment.attachmentId,
|
|
87
|
+
})
|
|
88
|
+
return toLosslessJson({
|
|
89
|
+
summary,
|
|
90
|
+
path: filePath,
|
|
91
|
+
url: localUrl,
|
|
92
|
+
width: res.width || attachment.width,
|
|
93
|
+
height: res.height || attachment.height,
|
|
94
|
+
format: 'png',
|
|
95
|
+
attachment,
|
|
96
|
+
})
|
|
97
|
+
} finally {
|
|
98
|
+
if (!committed) {
|
|
99
|
+
resvNobg.release()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
91
102
|
} catch (err) {
|
|
92
103
|
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
93
104
|
}
|
|
@@ -127,38 +138,49 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
127
138
|
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
128
139
|
const cfg = live()
|
|
129
140
|
const resvUpscale = reserveSpend(0.025, cfg.dailyBudgetUsd)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
filePath,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
141
|
+
let committed = false
|
|
142
|
+
try {
|
|
143
|
+
const res = await upscaleImageFal(
|
|
144
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
145
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec?.signal }
|
|
146
|
+
)
|
|
147
|
+
if (!res || !res.bytes) {
|
|
148
|
+
throw new Error('upscale_image provider returned empty or invalid response')
|
|
149
|
+
}
|
|
150
|
+
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
151
|
+
const name = `${stem}.png`
|
|
152
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
153
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
154
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
155
|
+
await mkdir(outDir, { recursive: true })
|
|
156
|
+
const filePath = path.join(outDir, name)
|
|
157
|
+
await writeFile(filePath, res.bytes)
|
|
158
|
+
resvUpscale.commit(0.025, { ctx, meta: { tool: 'upscale_image', scale: args.scale } })
|
|
159
|
+
committed = true
|
|
160
|
+
const summary = buildDualOutputMarkdown({
|
|
161
|
+
action: `upscaled (${args.scale || 2}x)`,
|
|
162
|
+
filePath,
|
|
163
|
+
width: res.width || attachment.width,
|
|
164
|
+
height: res.height || attachment.height,
|
|
165
|
+
mediaType: 'image/png',
|
|
166
|
+
provider: 'fal',
|
|
167
|
+
model: 'fal-ai/clarity-upscaler',
|
|
168
|
+
attachmentId: attachment.attachmentId,
|
|
169
|
+
})
|
|
170
|
+
return toLosslessJson({
|
|
171
|
+
summary,
|
|
172
|
+
path: filePath,
|
|
173
|
+
url: localUrl,
|
|
174
|
+
scale: args.scale || 2,
|
|
175
|
+
width: res.width || attachment.width,
|
|
176
|
+
height: res.height || attachment.height,
|
|
177
|
+
attachment,
|
|
178
|
+
})
|
|
179
|
+
} finally {
|
|
180
|
+
if (!committed) {
|
|
181
|
+
resvUpscale.release()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
162
184
|
} catch (err) {
|
|
163
185
|
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
164
186
|
}
|
|
@@ -254,50 +276,61 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
254
276
|
const cfg = live()
|
|
255
277
|
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
256
278
|
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
257
|
-
const resvBlend = reserveSpend(0.05, cfg.dailyBudgetUsd)
|
|
258
279
|
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
280
|
+
if (refs.length < 2) {
|
|
281
|
+
throw new Error('At least two images are required for blending')
|
|
282
|
+
}
|
|
283
|
+
const sources = []
|
|
284
|
+
for (const r of refs) {
|
|
285
|
+
const s = await resolveSource(ctx, exec, r)
|
|
286
|
+
if (s && s.bytes) sources.push(s)
|
|
287
|
+
}
|
|
288
|
+
if (sources.length < 2) {
|
|
289
|
+
throw new Error('At least two valid images are required to blend')
|
|
290
|
+
}
|
|
291
|
+
const resvBlend = reserveSpend(0.05, cfg.dailyBudgetUsd)
|
|
292
|
+
let committed = false
|
|
293
|
+
try {
|
|
294
|
+
const res = await blendImagesFal(
|
|
295
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
296
|
+
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec?.signal }
|
|
297
|
+
)
|
|
298
|
+
if (!res || !res.bytes) {
|
|
299
|
+
throw new Error('blend_images provider returned empty or invalid response')
|
|
300
|
+
}
|
|
301
|
+
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
302
|
+
const name = `${stem}.png`
|
|
303
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
304
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
305
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
306
|
+
await mkdir(outDir, { recursive: true })
|
|
307
|
+
const filePath = path.join(outDir, name)
|
|
308
|
+
await writeFile(filePath, res.bytes)
|
|
309
|
+
resvBlend.commit(0.05, { ctx, meta: { tool: 'blend_images' } })
|
|
310
|
+
committed = true
|
|
311
|
+
const summary = buildDualOutputMarkdown({
|
|
312
|
+
action: 'blended composition',
|
|
313
|
+
filePath,
|
|
314
|
+
width: res.width || attachment.width,
|
|
315
|
+
height: res.height || attachment.height,
|
|
316
|
+
mediaType: 'image/png',
|
|
317
|
+
provider: 'fal',
|
|
318
|
+
model: 'fal-ai/image-blend',
|
|
319
|
+
attachmentId: attachment.attachmentId,
|
|
320
|
+
})
|
|
321
|
+
return toLosslessJson({
|
|
322
|
+
summary,
|
|
323
|
+
path: filePath,
|
|
324
|
+
url: localUrl,
|
|
325
|
+
width: res.width || attachment.width,
|
|
326
|
+
height: res.height || attachment.height,
|
|
327
|
+
attachment,
|
|
328
|
+
})
|
|
329
|
+
} finally {
|
|
330
|
+
if (!committed) {
|
|
331
|
+
resvBlend.release()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
301
334
|
} catch (err) {
|
|
302
335
|
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
303
336
|
}
|
|
@@ -146,7 +146,8 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
146
146
|
count: cellCount,
|
|
147
147
|
})
|
|
148
148
|
const batchReservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
149
|
-
|
|
149
|
+
let committed = false
|
|
150
|
+
try {
|
|
150
151
|
const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
151
152
|
const quadrantIds = ['A', 'B', 'C', 'D']
|
|
152
153
|
const matrixId = `matrix-${Date.now().toString(36)}`
|
|
@@ -236,6 +237,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
236
237
|
cells,
|
|
237
238
|
})
|
|
238
239
|
|
|
240
|
+
committed = true
|
|
239
241
|
return toLosslessJson({
|
|
240
242
|
matrix_id: matrixId,
|
|
241
243
|
base_prompt: basePrompt,
|
|
@@ -244,6 +246,11 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
244
246
|
cells,
|
|
245
247
|
summary,
|
|
246
248
|
})
|
|
249
|
+
} finally {
|
|
250
|
+
if (!committed) {
|
|
251
|
+
batchReservation.release()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
247
254
|
},
|
|
248
255
|
}),
|
|
249
256
|
)
|
package/lib/tools/theme-pair.js
CHANGED
|
@@ -154,7 +154,8 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
154
154
|
count: 2,
|
|
155
155
|
})
|
|
156
156
|
const batchReservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
157
|
-
|
|
157
|
+
let committed = false
|
|
158
|
+
try {
|
|
158
159
|
const chain = resolveFallbackChain(primaryProvider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
159
160
|
const baseSeed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 1000000)
|
|
160
161
|
|
|
@@ -277,6 +278,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
277
278
|
+ `- Dark: ${darkFilename} (seed: ${baseSeed})\n\n`
|
|
278
279
|
+ `\`\`\`html\n${html}\n\`\`\``
|
|
279
280
|
|
|
281
|
+
committed = true
|
|
280
282
|
return toLosslessJson({
|
|
281
283
|
summary,
|
|
282
284
|
prompt,
|
|
@@ -302,6 +304,11 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
302
304
|
css_snippet: css,
|
|
303
305
|
attachment: lightAttachment,
|
|
304
306
|
})
|
|
307
|
+
} finally {
|
|
308
|
+
if (!committed) {
|
|
309
|
+
batchReservation.release()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
305
312
|
},
|
|
306
313
|
}),
|
|
307
314
|
)
|
package/lib/tools/ui-asset.js
CHANGED
|
@@ -173,14 +173,11 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
173
173
|
const seedVal = args.seed ?? Math.floor(Math.random() * 100000)
|
|
174
174
|
|
|
175
175
|
let gen
|
|
176
|
+
let committed = false
|
|
176
177
|
try {
|
|
177
178
|
gen = await executeWithFallback(providers, chain, seedVal, synthesizedPrompt, {
|
|
178
179
|
logger: ctx.logger,
|
|
179
180
|
})
|
|
180
|
-
} catch (err) {
|
|
181
|
-
reservation.release()
|
|
182
|
-
throw err
|
|
183
|
-
}
|
|
184
181
|
|
|
185
182
|
let finalBytes = gen.bytes
|
|
186
183
|
let mediaType = gen.mediaType || 'image/png'
|
|
@@ -220,6 +217,7 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
220
217
|
ctx,
|
|
221
218
|
meta: { provider, model: cfg.model || cfg.customModel, prompt: synthesizedPrompt, seed: seedVal },
|
|
222
219
|
})
|
|
220
|
+
committed = true
|
|
223
221
|
|
|
224
222
|
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
225
223
|
const targetDir = cfg.outputDir || 'generated/images'
|
|
@@ -274,6 +272,11 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
274
272
|
attachment,
|
|
275
273
|
_fallback: gen._fallback,
|
|
276
274
|
})
|
|
275
|
+
} finally {
|
|
276
|
+
if (!committed) {
|
|
277
|
+
reservation.release()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
277
280
|
},
|
|
278
281
|
}),
|
|
279
282
|
)
|
package/lib/tools/vision-ocr.js
CHANGED
|
@@ -254,6 +254,7 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
254
254
|
})
|
|
255
255
|
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
256
256
|
let gen
|
|
257
|
+
let committed = false
|
|
257
258
|
try {
|
|
258
259
|
gen = await executeWithFallback(
|
|
259
260
|
providers,
|
|
@@ -262,10 +263,6 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
262
263
|
inpaintPrompt,
|
|
263
264
|
{ logger: ctx.logger },
|
|
264
265
|
)
|
|
265
|
-
} catch (err) {
|
|
266
|
-
reservation.release()
|
|
267
|
-
throw err
|
|
268
|
-
}
|
|
269
266
|
|
|
270
267
|
const inpaintedBytes = gen.bytes
|
|
271
268
|
if (!inpaintedBytes) {
|
|
@@ -332,6 +329,7 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
332
329
|
ctx,
|
|
333
330
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: inpaintPrompt, seed },
|
|
334
331
|
})
|
|
332
|
+
committed = true
|
|
335
333
|
|
|
336
334
|
const dualOutput = buildDualOutputMarkdown({
|
|
337
335
|
action: 'generated',
|
|
@@ -364,6 +362,11 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
364
362
|
}
|
|
365
363
|
|
|
366
364
|
return toLosslessJson(response)
|
|
365
|
+
} finally {
|
|
366
|
+
if (!committed) {
|
|
367
|
+
reservation.release()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
367
370
|
},
|
|
368
371
|
})
|
|
369
372
|
)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.11.
|
|
4
|
-
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers
|
|
3
|
+
"version": "0.11.32",
|
|
4
|
+
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers \u2014 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",
|
|
7
7
|
"dsh",
|