@goodandready/dsh-image-gen 0.11.29 → 0.11.30
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 +129 -67
- package/lib/generation-helpers.js +1 -0
- package/lib/history.js +31 -5
- package/lib/live-progress.js +5 -3
- package/lib/loop-guard.js +20 -8
- package/lib/providers/shared-helpers.js +0 -3
- package/lib/providers.js +1 -13
- package/lib/tools/editing.js +6 -3
- package/lib/tools/generation.js +7 -2
- package/lib/tools/pattern.js +2 -1
- package/lib/tools/processing-basic.js +2 -1
- package/lib/tools/responsive.js +2 -1
- package/lib/tools/sketch.js +2 -1
- package/lib/tools/spritesheet.js +2 -1
- package/lib/tools/vision-ocr.js +28 -7
- package/package.json +1 -1
package/lib/frontend-assets.js
CHANGED
|
@@ -70,7 +70,7 @@ export function getContrastRatio(hex1, hex2) {
|
|
|
70
70
|
* @returns {Promise<string[]>} Hex colors
|
|
71
71
|
*/
|
|
72
72
|
export async function extractSampleColorsFromBuffer(buffer) {
|
|
73
|
-
if (!buffer || buffer.length
|
|
73
|
+
if (!buffer || buffer.length === 0) {
|
|
74
74
|
return ['#0f172a', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#f8fafc']
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -96,76 +96,79 @@ export async function extractSampleColorsFromBuffer(buffer) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Decode raster container to raw RGBA pixels via Sharp
|
|
99
|
+
let data, info
|
|
99
100
|
try {
|
|
100
|
-
const
|
|
101
|
+
const res = await sharp(buffer)
|
|
101
102
|
.raw()
|
|
102
103
|
.ensureAlpha()
|
|
103
104
|
.toBuffer({ resolveWithObject: true })
|
|
105
|
+
data = res.data
|
|
106
|
+
info = res.info
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new Error(`Failed to decode image buffer for color extraction: ${err.message}`)
|
|
109
|
+
}
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
bucket.count++
|
|
131
|
-
bucket.rSum += r
|
|
132
|
-
bucket.gSum += g
|
|
133
|
-
bucket.bSum += b
|
|
111
|
+
const totalPixels = info.width * info.height
|
|
112
|
+
const step = Math.max(1, Math.floor(totalPixels / 2048))
|
|
113
|
+
const colorBuckets = new Map()
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < totalPixels; i += step) {
|
|
116
|
+
const off = i * 4
|
|
117
|
+
const r = data[off]
|
|
118
|
+
const g = data[off + 1]
|
|
119
|
+
const b = data[off + 2]
|
|
120
|
+
const a = data[off + 3]
|
|
121
|
+
|
|
122
|
+
// Ignore fully or mostly transparent pixels
|
|
123
|
+
if (a < 128) continue
|
|
124
|
+
|
|
125
|
+
// Quantize to 16-level RGB bins to cluster nearby nuances
|
|
126
|
+
const qr = Math.round(r / 16) * 16
|
|
127
|
+
const qg = Math.round(g / 16) * 16
|
|
128
|
+
const qb = Math.round(b / 16) * 16
|
|
129
|
+
const key = `${qr},${qg},${qb}`
|
|
130
|
+
|
|
131
|
+
let bucket = colorBuckets.get(key)
|
|
132
|
+
if (!bucket) {
|
|
133
|
+
bucket = { count: 0, rSum: 0, gSum: 0, bSum: 0 }
|
|
134
|
+
colorBuckets.set(key, bucket)
|
|
134
135
|
}
|
|
136
|
+
bucket.count++
|
|
137
|
+
bucket.rSum += r
|
|
138
|
+
bucket.gSum += g
|
|
139
|
+
bucket.bSum += b
|
|
140
|
+
}
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
if (colorBuckets.size > 0) {
|
|
143
|
+
const sorted = Array.from(colorBuckets.values()).sort((a, b) => b.count - a.count)
|
|
144
|
+
const dominant = sorted.slice(0, 6).map((b) => {
|
|
145
|
+
return rgbToHex(b.rSum / b.count, b.gSum / b.count, b.bSum / b.count)
|
|
146
|
+
})
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
148
|
+
const result = [...dominant]
|
|
149
|
+
if (result.length < 5) {
|
|
150
|
+
const primaryRgb = hexToRgb(result[0])
|
|
151
|
+
const isNearBlack = primaryRgb.r === 0 && primaryRgb.g === 0 && primaryRgb.b === 0
|
|
152
|
+
if (isNearBlack) {
|
|
153
|
+
const darkShades = ['#1a1a1a', '#333333', '#4d4d4d', '#666666', '#808080']
|
|
154
|
+
for (const s of darkShades) {
|
|
155
|
+
if (result.length >= 6) break
|
|
156
|
+
if (!result.includes(s)) result.push(s)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
const factors = [0.2, 0.4, 0.6, 0.8, 1.2, 1.4]
|
|
160
|
+
for (const factor of factors) {
|
|
161
|
+
if (result.length >= 6) break
|
|
162
|
+
const shade = rgbToHex(
|
|
163
|
+
Math.min(255, Math.round(primaryRgb.r * factor)),
|
|
164
|
+
Math.min(255, Math.round(primaryRgb.g * factor)),
|
|
165
|
+
Math.min(255, Math.round(primaryRgb.b * factor)),
|
|
166
|
+
)
|
|
167
|
+
if (!result.includes(shade)) result.push(shade)
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
|
-
return result.slice(0, 6)
|
|
166
170
|
}
|
|
167
|
-
|
|
168
|
-
// If sharp cannot decode or buffer is non-image, fallback
|
|
171
|
+
return result.slice(0, 6)
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
return ['#0f172a', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#f8fafc']
|
|
@@ -334,6 +337,28 @@ export function checkWcagContrast(colors, textHex = '#ffffff') {
|
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Decode common HTML numeric and named entities in SVG attribute values (#374)
|
|
342
|
+
* @param {string} str
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
export function decodeHtmlEntities(str) {
|
|
346
|
+
if (!str || typeof str !== 'string') return ''
|
|
347
|
+
return str
|
|
348
|
+
.replace(/&#x([0-9a-fA-F]+);?/gi, (_, hex) => {
|
|
349
|
+
try { return String.fromCharCode(parseInt(hex, 16)) } catch (_e) { return '' }
|
|
350
|
+
})
|
|
351
|
+
.replace(/&#([0-9]+);?/g, (_, dec) => {
|
|
352
|
+
try { return String.fromCharCode(parseInt(dec, 10)) } catch (_e) { return '' }
|
|
353
|
+
})
|
|
354
|
+
.replace(/:/gi, ':')
|
|
355
|
+
.replace(/&tab;/gi, '\t')
|
|
356
|
+
.replace(/&newline;/gi, '\n')
|
|
357
|
+
.replace(/"/gi, '"')
|
|
358
|
+
.replace(/'/gi, "'")
|
|
359
|
+
.replace(/&/gi, '&')
|
|
360
|
+
}
|
|
361
|
+
|
|
337
362
|
/**
|
|
338
363
|
* Issue #176: optimize_vector_svg
|
|
339
364
|
* Cleans, sanitizes, and minifies SVG content, normalizes viewBox, and exports React TSX component.
|
|
@@ -360,14 +385,51 @@ export function optimizeSvgContent(svgString, options = {}) {
|
|
|
360
385
|
cleaned = cleaned.replace(/<script[^>]*\/?>/gi, '')
|
|
361
386
|
cleaned = cleaned.replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, '')
|
|
362
387
|
cleaned = cleaned.replace(/<foreignObject[^>]*\/?>/gi, '')
|
|
363
|
-
cleaned = cleaned.replace(/<(?:iframe|embed|object|applet)[\s\S]*?<\/(?:iframe|embed|object|applet)>/gi, '')
|
|
364
|
-
cleaned = cleaned.replace(/<(?:iframe|embed|object|applet)[^>]*\/?>/gi, '')
|
|
365
|
-
|
|
366
|
-
// Strip
|
|
367
|
-
cleaned = cleaned.replace(
|
|
388
|
+
cleaned = cleaned.replace(/<(?:iframe|embed|object|applet|handler|listener)[\s\S]*?<\/(?:iframe|embed|object|applet|handler|listener)>/gi, '')
|
|
389
|
+
cleaned = cleaned.replace(/<(?:iframe|embed|object|applet|handler|listener)[^>]*\/?>/gi, '')
|
|
390
|
+
|
|
391
|
+
// Security (#374): Strip dangerous SMIL animation elements (<animate>, <set>, etc.) targeting href/xlink:href/on* or containing script schemes
|
|
392
|
+
cleaned = cleaned.replace(/<(animate|set|animateTransform|animateMotion|discard)\b[^>]*>[\s\S]*?<\/\1>/gi, (block) => {
|
|
393
|
+
const norm = decodeHtmlEntities(block).replace(/[\s\x00-\x20]+/g, ' ').toLowerCase()
|
|
394
|
+
if (
|
|
395
|
+
/attributename\s*=\s*['"]?(?:href|xlink:href|src|action|formaction|on[a-z0-9_-]+)/.test(norm) ||
|
|
396
|
+
/(?:javascript|vbscript|data\s*:\s*text\/html)/.test(norm)
|
|
397
|
+
) {
|
|
398
|
+
return ''
|
|
399
|
+
}
|
|
400
|
+
return block
|
|
401
|
+
})
|
|
402
|
+
cleaned = cleaned.replace(/<(?:animate|set|animateTransform|animateMotion|discard)\b[^>]*\/?>/gi, (tag) => {
|
|
403
|
+
const norm = decodeHtmlEntities(tag).replace(/[\s\x00-\x20]+/g, ' ').toLowerCase()
|
|
404
|
+
if (
|
|
405
|
+
/attributename\s*=\s*['"]?(?:href|xlink:href|src|action|formaction|on[a-z0-9_-]+)/.test(norm) ||
|
|
406
|
+
/(?:javascript|vbscript|data\s*:\s*text\/html)/.test(norm)
|
|
407
|
+
) {
|
|
408
|
+
return ''
|
|
409
|
+
}
|
|
410
|
+
return tag
|
|
411
|
+
})
|
|
368
412
|
|
|
369
|
-
// Strip
|
|
370
|
-
cleaned = cleaned.replace(/\s+
|
|
413
|
+
// Strip event handler attributes (onload, onerror, onclick, etc., quoted or unquoted)
|
|
414
|
+
cleaned = cleaned.replace(/\s+on[a-z0-9_-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
415
|
+
|
|
416
|
+
// Security (#374): Strip dangerous URI schemes (javascript:, vbscript:, data:text/html, etc.) in href/xlink:href/src/action/formaction
|
|
417
|
+
// Handles HTML entity encoding (s), control chars, and unquoted attributes
|
|
418
|
+
cleaned = cleaned.replace(/\s+(?:href|xlink:href|src|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (match, q1, q2, unquoted) => {
|
|
419
|
+
const val = q1 !== undefined ? q1 : (q2 !== undefined ? q2 : unquoted)
|
|
420
|
+
const decoded = decodeHtmlEntities(val)
|
|
421
|
+
const normalized = decoded.replace(/[\x00-\x20\s]+/g, '').toLowerCase()
|
|
422
|
+
if (
|
|
423
|
+
normalized.startsWith('javascript:') ||
|
|
424
|
+
normalized.startsWith('vbscript:') ||
|
|
425
|
+
normalized.startsWith('data:text/html') ||
|
|
426
|
+
normalized.startsWith('data:text/javascript') ||
|
|
427
|
+
normalized.startsWith('data:image/svg+xml')
|
|
428
|
+
) {
|
|
429
|
+
return ''
|
|
430
|
+
}
|
|
431
|
+
return match
|
|
432
|
+
})
|
|
371
433
|
|
|
372
434
|
cleaned = cleaned.replace(/\s{2,}/g, ' ')
|
|
373
435
|
cleaned = cleaned.replace(/>\s+</g, '><')
|
package/lib/history.js
CHANGED
|
@@ -37,13 +37,38 @@ export function findCachedGeneration(entries, hash) {
|
|
|
37
37
|
return entries.find((e) => e.cacheHash === hash)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export async function findCachedByPrompt(entries, prompt) {
|
|
41
|
-
|
|
40
|
+
export async function findCachedByPrompt(entries, prompt, criteria = {}) {
|
|
41
|
+
if (!prompt || !Array.isArray(entries)) return undefined
|
|
42
|
+
return entries.find((e) => {
|
|
43
|
+
if (e.prompt !== prompt) return false
|
|
44
|
+
if (criteria.format) {
|
|
45
|
+
const entryFmt = (e.format || (e.mediaType || '').replace('image/', '')).toLowerCase().replace('jpeg', 'jpg')
|
|
46
|
+
const targetFmt = String(criteria.format).toLowerCase().replace('jpeg', 'jpg')
|
|
47
|
+
if (entryFmt && targetFmt && entryFmt !== targetFmt) return false
|
|
48
|
+
}
|
|
49
|
+
if (criteria.aspectRatio && e.aspectRatio && e.aspectRatio !== criteria.aspectRatio) return false
|
|
50
|
+
if (criteria.width && e.width && criteria.width !== e.width) return false
|
|
51
|
+
if (criteria.height && e.height && criteria.height !== e.height) return false
|
|
52
|
+
if (criteria.size && e.size && criteria.size !== criteria.size) return false
|
|
53
|
+
return true
|
|
54
|
+
})
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
export async function findCached(entries, seed, prompt) {
|
|
45
|
-
if (seed === undefined) return undefined
|
|
46
|
-
return entries.find((e) =>
|
|
57
|
+
export async function findCached(entries, seed, prompt, criteria = {}) {
|
|
58
|
+
if (seed === undefined || !Array.isArray(entries)) return undefined
|
|
59
|
+
return entries.find((e) => {
|
|
60
|
+
if (e.seed !== seed || e.prompt !== prompt) return false
|
|
61
|
+
if (criteria.format) {
|
|
62
|
+
const entryFmt = (e.format || (e.mediaType || '').replace('image/', '')).toLowerCase().replace('jpeg', 'jpg')
|
|
63
|
+
const targetFmt = String(criteria.format).toLowerCase().replace('jpeg', 'jpg')
|
|
64
|
+
if (entryFmt && targetFmt && entryFmt !== targetFmt) return false
|
|
65
|
+
}
|
|
66
|
+
if (criteria.aspectRatio && e.aspectRatio && e.aspectRatio !== criteria.aspectRatio) return false
|
|
67
|
+
if (criteria.width && e.width && criteria.width !== e.width) return false
|
|
68
|
+
if (criteria.height && e.height && criteria.height !== e.height) return false
|
|
69
|
+
if (criteria.size && e.size && criteria.size !== criteria.size) return false
|
|
70
|
+
return true
|
|
71
|
+
})
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
/** Return cached entry if file exists on disk, otherwise undefined. */
|
|
@@ -61,6 +86,7 @@ export async function cachedResult(entry) {
|
|
|
61
86
|
cost: 0,
|
|
62
87
|
format: (entry.mediaType || 'image/png').replace('image/', ''),
|
|
63
88
|
cached: true,
|
|
89
|
+
fromCache: true,
|
|
64
90
|
attachment: entry.attachmentId ? {
|
|
65
91
|
attachmentId: entry.attachmentId,
|
|
66
92
|
mediaType: entry.mediaType || 'image/png',
|
package/lib/live-progress.js
CHANGED
|
@@ -14,8 +14,10 @@ const activeSessions = new Map()
|
|
|
14
14
|
export function createLiveProgressSession(callId, options = {}) {
|
|
15
15
|
if (!callId) return null
|
|
16
16
|
const existing = activeSessions.get(callId)
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const listeners = existing?.listeners ? existing.listeners : new Set()
|
|
18
|
+
|
|
19
|
+
if (typeof options.onProgress === 'function') {
|
|
20
|
+
listeners.add(options.onProgress)
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const session = {
|
|
@@ -27,7 +29,7 @@ export function createLiveProgressSession(callId, options = {}) {
|
|
|
27
29
|
done: false,
|
|
28
30
|
error: undefined,
|
|
29
31
|
timestamp: Date.now(),
|
|
30
|
-
listeners
|
|
32
|
+
listeners,
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
if (options.signal) {
|
package/lib/loop-guard.js
CHANGED
|
@@ -41,15 +41,27 @@ export function trackAndAssertLoopGuard(sessionId, {
|
|
|
41
41
|
const current = sessionStates.get(sid) || { count: 0, lastPrompt: '', lastTimestamp: 0, lastTurnId: null }
|
|
42
42
|
const maxLimit = Math.max(1, Number(limit) || DEFAULT_MAX_CONSECUTIVE_GENERATIONS)
|
|
43
43
|
|
|
44
|
-
// Derive turn identifier from turnId or exec context
|
|
44
|
+
// Derive turn identifier from turnId or exec context (#370)
|
|
45
45
|
const session = exec?.agent?.session
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
let derivedTurnId = turnId ?? exec?.turnId ?? exec?.agent?.turnId
|
|
47
|
+
|
|
48
|
+
if (derivedTurnId === null || derivedTurnId === undefined) {
|
|
49
|
+
const rawMessages = typeof session?.deriveMessages === 'function'
|
|
50
|
+
? session.deriveMessages()
|
|
51
|
+
: (Array.isArray(session?.messages) ? session.messages : null)
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(rawMessages)) {
|
|
54
|
+
const userMessages = rawMessages.filter(m => m && (m.role === 'user' || m.type === 'user'))
|
|
55
|
+
if (userMessages.length > 0) {
|
|
56
|
+
const lastUser = userMessages[userMessages.length - 1]
|
|
57
|
+
derivedTurnId = lastUser.id ?? lastUser.turnId ?? `user_turn_${userMessages.length}`
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (derivedTurnId === null || derivedTurnId === undefined) {
|
|
63
|
+
derivedTurnId = session?.lastUserMessageId ?? session?.lastMessageId ?? session?.turnId ?? null
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
// If turn identifier changed, reset count for the new user turn
|
|
55
67
|
if (derivedTurnId !== null && derivedTurnId !== undefined) {
|
|
@@ -90,9 +90,6 @@ export async function ensureImageFormat(bytes, targetFormat, { fallbackMime } =
|
|
|
90
90
|
const verified = detectImageMediaType(transcoded) || targetMime
|
|
91
91
|
return { bytes: transcoded, mediaType: verified }
|
|
92
92
|
} catch (err) {
|
|
93
|
-
if (buf.length < 64) {
|
|
94
|
-
return { bytes: buf, mediaType: fallbackMime || targetMime || actualMime || 'image/png' }
|
|
95
|
-
}
|
|
96
93
|
throw new Error(`Failed to encode image to ${canonical}: ${err.message}`)
|
|
97
94
|
}
|
|
98
95
|
}
|
package/lib/providers.js
CHANGED
|
@@ -536,19 +536,7 @@ export async function blendImageBuffers(images, weights) {
|
|
|
536
536
|
const bytes = await sharp(out, { raw: { width: targetW, height: targetH, channels: 4 } }).png().toBuffer()
|
|
537
537
|
return { bytes, width: targetW, height: targetH, mediaType: 'image/png', weights: normWeights }
|
|
538
538
|
} catch (err) {
|
|
539
|
-
|
|
540
|
-
const maxLen = Math.max(...images.map(img => (img.bytes?.length || 0)))
|
|
541
|
-
const out = Buffer.alloc(maxLen)
|
|
542
|
-
for (let p = 0; p < maxLen; p++) {
|
|
543
|
-
let val = 0
|
|
544
|
-
for (let i = 0; i < images.length; i++) {
|
|
545
|
-
const b = images[i].bytes || Buffer.alloc(0)
|
|
546
|
-
const byteVal = p < b.length ? b[p] : 0
|
|
547
|
-
val += byteVal * normWeights[i]
|
|
548
|
-
}
|
|
549
|
-
out[p] = Math.round(Math.min(255, Math.max(0, val)))
|
|
550
|
-
}
|
|
551
|
-
return { bytes: out, width: 0, height: 0, mediaType: 'image/png', weights: normWeights }
|
|
539
|
+
throw new Error(`Failed to decode image buffer for blending: ${err.message}`)
|
|
552
540
|
}
|
|
553
541
|
}
|
|
554
542
|
|
package/lib/tools/editing.js
CHANGED
|
@@ -87,7 +87,8 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
87
87
|
if (cfg && cfg.enabled === false) {
|
|
88
88
|
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
91
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
91
92
|
const estimatedCostEdit = calculateGenerationCost({
|
|
92
93
|
provider: cfg.provider || 'fal',
|
|
93
94
|
model: cfg.model,
|
|
@@ -242,7 +243,8 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
242
243
|
if (cfg && cfg.enabled === false) {
|
|
243
244
|
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
244
245
|
}
|
|
245
|
-
|
|
246
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
247
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
246
248
|
const count = normalizeCount(args.count || 1)
|
|
247
249
|
const estimatedCostVary = calculateGenerationCost({
|
|
248
250
|
provider: cfg.provider || 'fal',
|
|
@@ -383,7 +385,8 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
383
385
|
if (cfg && cfg.enabled === false) {
|
|
384
386
|
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
385
387
|
}
|
|
386
|
-
|
|
388
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
389
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
387
390
|
const count = normalizeCount(args.count || 1)
|
|
388
391
|
const estimatedCost = calculateGenerationCost({
|
|
389
392
|
provider: cfg.provider || 'fal',
|
package/lib/tools/generation.js
CHANGED
|
@@ -500,12 +500,17 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
500
500
|
const hPrompt = h
|
|
501
501
|
const found = findCachedGeneration(historyEntries, hPrompt)
|
|
502
502
|
if (found) return await cachedResult(found)
|
|
503
|
+
const requestedCriteria = {
|
|
504
|
+
format: args.output_format || cfg.defaultFormat || 'png',
|
|
505
|
+
aspectRatio: args.aspect_ratio || cfg.defaultAspectRatio,
|
|
506
|
+
size: args.aspect_ratio ? 'custom' : (args.image_size || cfg.defaultSize),
|
|
507
|
+
}
|
|
503
508
|
if (cfg.cacheBySeed) {
|
|
504
|
-
const bySeed = await findCached(historyEntries, seedVal, promptVal)
|
|
509
|
+
const bySeed = await findCached(historyEntries, seedVal, promptVal, requestedCriteria)
|
|
505
510
|
if (bySeed) return await cachedResult(bySeed)
|
|
506
511
|
}
|
|
507
512
|
if (cfg.cacheByPrompt) {
|
|
508
|
-
const byPrompt = await findCachedByPrompt(historyEntries, promptVal)
|
|
513
|
+
const byPrompt = await findCachedByPrompt(historyEntries, promptVal, requestedCriteria)
|
|
509
514
|
if (byPrompt) return await cachedResult(byPrompt)
|
|
510
515
|
}
|
|
511
516
|
return undefined
|
package/lib/tools/pattern.js
CHANGED
|
@@ -67,7 +67,8 @@ export function registerPatternTools(ctx, deps) {
|
|
|
67
67
|
async execute(args, exec) {
|
|
68
68
|
try {
|
|
69
69
|
const cfg = live()
|
|
70
|
-
|
|
70
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
71
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.motif || '', exec })
|
|
71
72
|
const density = normalizeDensity(args.density)
|
|
72
73
|
const prompt = buildSeamlessPrompt(args.motif, args.style, density)
|
|
73
74
|
const provider = cfg.provider || 'fal'
|
|
@@ -252,7 +252,8 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
252
252
|
async execute(args, exec) {
|
|
253
253
|
try {
|
|
254
254
|
const cfg = live()
|
|
255
|
-
|
|
255
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
256
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
256
257
|
assertBudgetAvailable(0.05, cfg.dailyBudgetUsd)
|
|
257
258
|
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
258
259
|
if (refs.length < 2) {
|
package/lib/tools/responsive.js
CHANGED
|
@@ -101,7 +101,8 @@ export function registerResponsiveTools(ctx, deps) {
|
|
|
101
101
|
async execute(args, exec) {
|
|
102
102
|
try {
|
|
103
103
|
const cfg = live()
|
|
104
|
-
|
|
104
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
105
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
105
106
|
if (!args.prompt || !String(args.prompt).trim()) {
|
|
106
107
|
throw new Error('prompt is required for generate_responsive_mockups')
|
|
107
108
|
}
|
package/lib/tools/sketch.js
CHANGED
|
@@ -87,7 +87,8 @@ export function registerSketchTools(ctx, deps) {
|
|
|
87
87
|
async execute(args, exec) {
|
|
88
88
|
try {
|
|
89
89
|
const cfg = live()
|
|
90
|
-
|
|
90
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
91
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
91
92
|
const estimated = calculateGenerationCost({
|
|
92
93
|
provider: cfg.provider || 'fal',
|
|
93
94
|
model: cfg.model,
|
package/lib/tools/spritesheet.js
CHANGED
|
@@ -74,7 +74,8 @@ export function registerSpritesheetTools(ctx, deps) {
|
|
|
74
74
|
async execute(args, exec) {
|
|
75
75
|
try {
|
|
76
76
|
const cfg = live()
|
|
77
|
-
|
|
77
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
78
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
78
79
|
const animation = String(args.animation || 'idle').toLowerCase()
|
|
79
80
|
const animKey = ANIMATION_PRESETS[animation] ? animation : 'idle'
|
|
80
81
|
const frameCount = clampFrames(args.frames, animKey)
|
package/lib/tools/vision-ocr.js
CHANGED
|
@@ -150,8 +150,18 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
150
150
|
throw new Error(`Unable to resolve source image for text replacement: ${args.image}`)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
let imgWidth = 1024
|
|
154
|
+
let imgHeight = 1024
|
|
155
|
+
try {
|
|
156
|
+
const sharpMod = await import('sharp')
|
|
157
|
+
const sharp = sharpMod.default || sharpMod
|
|
158
|
+
const meta = await sharp(source.bytes).metadata()
|
|
159
|
+
imgWidth = meta.width || source.width || 1024
|
|
160
|
+
imgHeight = meta.height || source.height || 1024
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
imgWidth = source.width || 1024
|
|
163
|
+
imgHeight = source.height || 1024
|
|
164
|
+
}
|
|
155
165
|
|
|
156
166
|
// 1. Gather text items and bounding boxes
|
|
157
167
|
const replacements = Array.isArray(args.replacements) ? args.replacements : []
|
|
@@ -162,13 +172,14 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
162
172
|
let bbox = item.bbox
|
|
163
173
|
if (!bbox && item.find && ctx.tools?.get) {
|
|
164
174
|
const groundTool = ctx.tools.get('vision_ground')
|
|
165
|
-
if (groundTool && typeof groundTool.execute === 'function'
|
|
175
|
+
if (groundTool && typeof groundTool.execute === 'function') {
|
|
166
176
|
try {
|
|
167
177
|
const gRes = await groundTool.execute({
|
|
168
|
-
|
|
178
|
+
image: args.image,
|
|
179
|
+
attachmentId: source.ref?.startsWith('sha256:') ? source.ref : (args.image?.startsWith('sha256:') ? args.image : undefined),
|
|
169
180
|
target: `text "${item.find}"`,
|
|
170
181
|
}, execCtx)
|
|
171
|
-
if (gRes?.bbox && gRes.bbox.length === 4) {
|
|
182
|
+
if (gRes?.bbox && Array.isArray(gRes.bbox) && gRes.bbox.length === 4) {
|
|
172
183
|
bbox = gRes.bbox
|
|
173
184
|
}
|
|
174
185
|
} catch (_err) {
|
|
@@ -258,13 +269,23 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
258
269
|
|
|
259
270
|
// 5. Composite typography overlay on inpainted background
|
|
260
271
|
let finalBytes = inpaintedBytes
|
|
272
|
+
let composited = false
|
|
261
273
|
try {
|
|
262
274
|
const sharpMod = await import('sharp')
|
|
263
275
|
const sharp = sharpMod.default || sharpMod
|
|
276
|
+
const inpaintMeta = await sharp(inpaintedBytes).metadata()
|
|
277
|
+
const compW = inpaintMeta.width || imgWidth
|
|
278
|
+
const compH = inpaintMeta.height || imgHeight
|
|
279
|
+
let compOverlay = overlayBuffer
|
|
280
|
+
if (compW !== imgWidth || compH !== imgHeight) {
|
|
281
|
+
const scaledOverlaySvg = buildTextOverlaySvg(compW, compH, overlayItems, args.font_family || 'sans-serif')
|
|
282
|
+
compOverlay = Buffer.from(scaledOverlaySvg, 'utf8')
|
|
283
|
+
}
|
|
264
284
|
finalBytes = await sharp(inpaintedBytes)
|
|
265
|
-
.composite([{ input:
|
|
285
|
+
.composite([{ input: compOverlay, top: 0, left: 0 }])
|
|
266
286
|
.png()
|
|
267
287
|
.toBuffer()
|
|
288
|
+
composited = true
|
|
268
289
|
} catch (sharpErr) {
|
|
269
290
|
if (ctx.logger?.warn) ctx.logger.warn(`Sharp compositing skipped: ${sharpErr.message}`)
|
|
270
291
|
}
|
|
@@ -322,7 +343,7 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
322
343
|
|
|
323
344
|
const response = {
|
|
324
345
|
summary: dualOutput,
|
|
325
|
-
replacedCount: overlayItems.length,
|
|
346
|
+
replacedCount: composited ? overlayItems.length : 0,
|
|
326
347
|
bboxes,
|
|
327
348
|
inpaintPrompt,
|
|
328
349
|
targetLanguage: args.target_language || '',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.30",
|
|
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",
|