@goodandready/dsh-image-gen 0.10.23 → 0.10.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.
- package/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/client.js +21 -21
- package/lib/cost-meter.js +4 -2
- package/lib/generation-cache.js +6 -3
- package/lib/index.js +2 -0
- package/lib/loop-guard.js +2 -0
- package/lib/prompt-polisher.js +0 -7
- package/lib/provider-utils.js +302 -0
- package/lib/providers.js +49 -294
- package/lib/register-tools.js +3 -1
- package/lib/tools/{processing.js → processing-advanced.js} +4 -269
- package/lib/tools/processing-basic.js +327 -0
- package/package.json +2 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// processing-basic — core image-gen tools (remove_background, upscale_image, vectorize_image, blend_images). Split from processing.js (#239).
|
|
2
|
+
|
|
3
|
+
// processing — image-gen tools (remove_background, upscale_image, vectorize_image, blend_images). Extracted from apply() (#216).
|
|
4
|
+
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
8
|
+
import {
|
|
9
|
+
computeGenerationHash,
|
|
10
|
+
IMAGE_SIZES,
|
|
11
|
+
OUTPUT_FORMATS,
|
|
12
|
+
PROVIDER_KEYS,
|
|
13
|
+
buildSidecar,
|
|
14
|
+
makeProviders,
|
|
15
|
+
ASPECT_RATIOS,
|
|
16
|
+
pixelDiff,
|
|
17
|
+
normalizeCount,
|
|
18
|
+
tryGenerate,
|
|
19
|
+
fallbackOrder,
|
|
20
|
+
embedPngMetadata,
|
|
21
|
+
removeBackgroundFal,
|
|
22
|
+
upscaleImageFal,
|
|
23
|
+
traceToSvg,
|
|
24
|
+
estimateCost,
|
|
25
|
+
estimateSharpnessAndVariance,
|
|
26
|
+
applyStylePreset,
|
|
27
|
+
resolveStylePreset,
|
|
28
|
+
blendImagesFal,
|
|
29
|
+
toLosslessJson,
|
|
30
|
+
saveAttachmentSafe,
|
|
31
|
+
} from '../providers.js'
|
|
32
|
+
import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
|
|
33
|
+
import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
|
|
34
|
+
import { executeWithQualityGate } from '../quality-gate.js'
|
|
35
|
+
import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
|
|
36
|
+
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
37
|
+
import { sanitizeErrorAndLogs } from '../security.js'
|
|
38
|
+
import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
|
|
39
|
+
import {
|
|
40
|
+
extractDesignTokens,
|
|
41
|
+
generateCssGradient,
|
|
42
|
+
checkWcagContrast,
|
|
43
|
+
optimizeSvgContent,
|
|
44
|
+
generatePwaIconSuite,
|
|
45
|
+
extractSampleColorsFromBuffer,
|
|
46
|
+
} from '../frontend-assets.js'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export function registerProcessingTools(ctx, deps) {
|
|
50
|
+
const {
|
|
51
|
+
config,
|
|
52
|
+
live,
|
|
53
|
+
saveAndAttachResult,
|
|
54
|
+
resolveSource,
|
|
55
|
+
slugify,
|
|
56
|
+
resolveApiKey,
|
|
57
|
+
} = deps
|
|
58
|
+
ctx.effect(() => {
|
|
59
|
+
ctx.tools.register(
|
|
60
|
+
defineTool({
|
|
61
|
+
name: 'remove_background',
|
|
62
|
+
description: 'Remove the background of an image (path or attachment id) and return a transparent PNG.',
|
|
63
|
+
parameters: {
|
|
64
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
65
|
+
model: { type: 'string', description: 'FAL model for background removal (default: fal-ai/birefnet).' },
|
|
66
|
+
output_name: { type: 'string', description: 'Custom output file name without extension.' },
|
|
67
|
+
},
|
|
68
|
+
output: {
|
|
69
|
+
schema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
additionalProperties: true,
|
|
72
|
+
properties: {
|
|
73
|
+
path: { type: 'string' },
|
|
74
|
+
url: { type: 'string' },
|
|
75
|
+
width: { type: 'number' },
|
|
76
|
+
height: { type: 'number' },
|
|
77
|
+
format: { type: 'string' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
async execute(args, exec) {
|
|
82
|
+
try {
|
|
83
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
84
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
85
|
+
const cfg = live()
|
|
86
|
+
const res = await removeBackgroundFal(
|
|
87
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
88
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec.signal }
|
|
89
|
+
)
|
|
90
|
+
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
91
|
+
const name = `${stem}.png`
|
|
92
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
93
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
94
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
95
|
+
await mkdir(outDir, { recursive: true })
|
|
96
|
+
const filePath = path.join(outDir, name)
|
|
97
|
+
await writeFile(filePath, res.bytes)
|
|
98
|
+
const summary = buildDualOutputMarkdown({
|
|
99
|
+
action: 'background removed',
|
|
100
|
+
filePath,
|
|
101
|
+
width: res.width || attachment.width,
|
|
102
|
+
height: res.height || attachment.height,
|
|
103
|
+
mediaType: 'image/png',
|
|
104
|
+
provider: 'fal',
|
|
105
|
+
model: args.model || 'fal-ai/birefnet',
|
|
106
|
+
attachmentId: attachment.attachmentId,
|
|
107
|
+
})
|
|
108
|
+
return toLosslessJson({
|
|
109
|
+
summary,
|
|
110
|
+
path: filePath,
|
|
111
|
+
url: localUrl,
|
|
112
|
+
width: res.width || attachment.width,
|
|
113
|
+
height: res.height || attachment.height,
|
|
114
|
+
format: 'png',
|
|
115
|
+
attachment,
|
|
116
|
+
})
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
}, 'dsh-image-gen: tool remove_background')
|
|
124
|
+
ctx.effect(() => {
|
|
125
|
+
ctx.tools.register(
|
|
126
|
+
defineTool({
|
|
127
|
+
name: 'upscale_image',
|
|
128
|
+
description: 'Upscale an image 2x or 4x with enhanced clarity and detail.',
|
|
129
|
+
parameters: {
|
|
130
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
131
|
+
scale: { type: 'number', description: 'Upscale factor: 2 or 4 (default: 2).' },
|
|
132
|
+
prompt: { type: 'string', description: 'Optional guiding prompt for detail reconstruction.' },
|
|
133
|
+
creativity: { type: 'number', description: 'Creativity / hallucination level from 0 to 1.' },
|
|
134
|
+
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
135
|
+
},
|
|
136
|
+
output: {
|
|
137
|
+
schema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
additionalProperties: true,
|
|
140
|
+
properties: {
|
|
141
|
+
path: { type: 'string' },
|
|
142
|
+
url: { type: 'string' },
|
|
143
|
+
scale: { type: 'number' },
|
|
144
|
+
width: { type: 'number' },
|
|
145
|
+
height: { type: 'number' },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
async execute(args, exec) {
|
|
150
|
+
try {
|
|
151
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
152
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
153
|
+
const cfg = live()
|
|
154
|
+
const res = await upscaleImageFal(
|
|
155
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
156
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec.signal }
|
|
157
|
+
)
|
|
158
|
+
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
159
|
+
const name = `${stem}.png`
|
|
160
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
161
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
162
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
163
|
+
await mkdir(outDir, { recursive: true })
|
|
164
|
+
const filePath = path.join(outDir, name)
|
|
165
|
+
await writeFile(filePath, res.bytes)
|
|
166
|
+
const summary = buildDualOutputMarkdown({
|
|
167
|
+
action: `upscaled (${args.scale || 2}x)`,
|
|
168
|
+
filePath,
|
|
169
|
+
width: res.width || attachment.width,
|
|
170
|
+
height: res.height || attachment.height,
|
|
171
|
+
mediaType: 'image/png',
|
|
172
|
+
provider: 'fal',
|
|
173
|
+
model: 'fal-ai/clarity-upscaler',
|
|
174
|
+
attachmentId: attachment.attachmentId,
|
|
175
|
+
})
|
|
176
|
+
return toLosslessJson({
|
|
177
|
+
summary,
|
|
178
|
+
path: filePath,
|
|
179
|
+
url: localUrl,
|
|
180
|
+
scale: args.scale || 2,
|
|
181
|
+
width: res.width || attachment.width,
|
|
182
|
+
height: res.height || attachment.height,
|
|
183
|
+
attachment,
|
|
184
|
+
})
|
|
185
|
+
} catch (err) {
|
|
186
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
}, 'dsh-image-gen: tool upscale_image')
|
|
192
|
+
ctx.effect(() => {
|
|
193
|
+
ctx.tools.register(
|
|
194
|
+
defineTool({
|
|
195
|
+
name: 'vectorize_image',
|
|
196
|
+
description: 'Convert a raster image or icon to SVG vector format.',
|
|
197
|
+
parameters: {
|
|
198
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
199
|
+
color_mode: { type: 'string', description: '"color" or "binary" (default: "color").' },
|
|
200
|
+
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
201
|
+
},
|
|
202
|
+
output: {
|
|
203
|
+
schema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
additionalProperties: true,
|
|
206
|
+
properties: {
|
|
207
|
+
path: { type: 'string' },
|
|
208
|
+
svg: { type: 'string' },
|
|
209
|
+
format: { type: 'string' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
async execute(args, exec) {
|
|
214
|
+
try {
|
|
215
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
216
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
217
|
+
const cfg = live()
|
|
218
|
+
const res = traceToSvg(source.bytes, { colorMode: args.color_mode })
|
|
219
|
+
const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
|
|
220
|
+
const name = `${stem}.svg`
|
|
221
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
222
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
223
|
+
await mkdir(outDir, { recursive: true })
|
|
224
|
+
const filePath = path.join(outDir, name)
|
|
225
|
+
await writeFile(filePath, res.bytes)
|
|
226
|
+
|
|
227
|
+
let attachment = null
|
|
228
|
+
let localUrl = ''
|
|
229
|
+
if (ctx.attachments?.saveImage) {
|
|
230
|
+
attachment = await ctx.attachments.saveImage({
|
|
231
|
+
data: new Uint8Array(res.bytes),
|
|
232
|
+
mediaType: 'image/svg+xml',
|
|
233
|
+
name,
|
|
234
|
+
})
|
|
235
|
+
localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const summary = `### Vectorized SVG Export\n- **File**: ${filePath}\n- **Format**: SVG (XML vector)\n- **Color mode**: ${args.color_mode || 'color'}\n- **Palette size**: ${res.palette ? res.palette.length : 16} colors\n`
|
|
239
|
+
return toLosslessJson({
|
|
240
|
+
summary,
|
|
241
|
+
path: filePath,
|
|
242
|
+
url: localUrl,
|
|
243
|
+
svg: res.svg,
|
|
244
|
+
format: 'svg',
|
|
245
|
+
attachment,
|
|
246
|
+
})
|
|
247
|
+
} catch (err) {
|
|
248
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
}, 'dsh-image-gen: tool vectorize_image')
|
|
254
|
+
ctx.effect(() => {
|
|
255
|
+
ctx.tools.register(
|
|
256
|
+
defineTool({
|
|
257
|
+
name: 'blend_images',
|
|
258
|
+
description: 'Blend multiple images (paths or attachment ids) into a coherent new composition.',
|
|
259
|
+
parameters: {
|
|
260
|
+
images: { type: 'array', items: { type: 'string' }, required: true, description: 'List of image paths or attachment ids to blend.' },
|
|
261
|
+
weights: { type: 'array', items: { type: 'number' }, description: 'Weights for each image (optional).' },
|
|
262
|
+
prompt: { type: 'string', description: 'Guiding prompt for the blend.' },
|
|
263
|
+
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
264
|
+
},
|
|
265
|
+
output: {
|
|
266
|
+
schema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
additionalProperties: true,
|
|
269
|
+
properties: {
|
|
270
|
+
path: { type: 'string' },
|
|
271
|
+
url: { type: 'string' },
|
|
272
|
+
width: { type: 'number' },
|
|
273
|
+
height: { type: 'number' },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
async execute(args, exec) {
|
|
278
|
+
try {
|
|
279
|
+
const cfg = live()
|
|
280
|
+
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
281
|
+
assertBudgetAvailable(0.05, cfg.dailyBudgetUsd)
|
|
282
|
+
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
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 === 0) throw new Error('No valid images found to blend')
|
|
289
|
+
const res = await blendImagesFal(
|
|
290
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
291
|
+
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec.signal }
|
|
292
|
+
)
|
|
293
|
+
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
294
|
+
const name = `${stem}.png`
|
|
295
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
296
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
297
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
298
|
+
await mkdir(outDir, { recursive: true })
|
|
299
|
+
const filePath = path.join(outDir, name)
|
|
300
|
+
await writeFile(filePath, res.bytes)
|
|
301
|
+
const summary = buildDualOutputMarkdown({
|
|
302
|
+
action: 'blended composition',
|
|
303
|
+
filePath,
|
|
304
|
+
width: res.width || attachment.width,
|
|
305
|
+
height: res.height || attachment.height,
|
|
306
|
+
mediaType: 'image/png',
|
|
307
|
+
provider: 'fal',
|
|
308
|
+
model: 'fal-ai/image-blend',
|
|
309
|
+
attachmentId: attachment.attachmentId,
|
|
310
|
+
})
|
|
311
|
+
return toLosslessJson({
|
|
312
|
+
summary,
|
|
313
|
+
path: filePath,
|
|
314
|
+
url: localUrl,
|
|
315
|
+
width: res.width || attachment.width,
|
|
316
|
+
height: res.height || attachment.height,
|
|
317
|
+
attachment,
|
|
318
|
+
})
|
|
319
|
+
} catch (err) {
|
|
320
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
)
|
|
325
|
+
}, 'dsh-image-gen: tool blend_images')
|
|
326
|
+
|
|
327
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers
|
|
3
|
+
"version": "0.10.24",
|
|
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",
|