@goodandready/dsh-image-gen 0.11.29 → 0.11.31
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/client.js +2 -2
- package/lib/config-schema.js +1 -1
- package/lib/cost-meter.js +74 -7
- package/lib/frontend-assets.js +129 -67
- package/lib/generation-helpers.js +9 -5
- package/lib/history.js +31 -5
- package/lib/live-progress.js +5 -3
- package/lib/loop-guard.js +20 -8
- package/lib/providers/backends/fal.js +10 -0
- package/lib/providers/backends/gemini.js +17 -9
- package/lib/providers/backends/local.js +18 -0
- package/lib/providers/shared-helpers.js +0 -3
- package/lib/providers.js +3 -14
- package/lib/quality-gate.js +19 -2
- package/lib/tools/character-sheet.js +16 -11
- package/lib/tools/diagram.js +16 -11
- package/lib/tools/editing.js +6 -3
- package/lib/tools/generation.js +27 -10
- package/lib/tools/pattern.js +2 -1
- package/lib/tools/processing-basic.js +9 -8
- package/lib/tools/responsive.js +2 -1
- package/lib/tools/sketch.js +2 -1
- package/lib/tools/spritesheet.js +2 -1
- package/lib/tools/style-matrix.js +3 -3
- package/lib/tools/theme-pair.js +4 -4
- package/lib/tools/ui-asset.js +12 -7
- package/lib/tools/vision-ocr.js +44 -18
- package/package.json +1 -1
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
|
|
|
@@ -791,7 +779,8 @@ export async function testProviderConnection(deps, providerId) {
|
|
|
791
779
|
if (p === 'gemini') {
|
|
792
780
|
const key = await resolveKey(cfg?.geminiKeyEnv || 'GEMINI_API_KEY')
|
|
793
781
|
if (!key) return { ok: false, state: 'not_configured', latencyMs: 0, message: 'Google Gemini API key not configured' }
|
|
794
|
-
|
|
782
|
+
let model = cfg?.geminiModel || 'gemini-3.1-flash-image'
|
|
783
|
+
if (model.startsWith('imagen-') || model.startsWith('gemini-2.0-flash-exp')) model = 'gemini-3.1-flash-image'
|
|
795
784
|
let res
|
|
796
785
|
try {
|
|
797
786
|
res = await fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${model}?key=${encodeURIComponent(key)}`, {
|
package/lib/quality-gate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lib/quality-gate.js
|
|
2
|
-
// Automatic Quality Gate and Silent Re-roll Engine (#166)
|
|
2
|
+
// Automatic Quality Gate and Silent Re-roll Engine (#166, #353)
|
|
3
3
|
|
|
4
4
|
import { estimateSharpnessAndVariance } from './providers.js'
|
|
5
5
|
|
|
@@ -97,7 +97,24 @@ export async function executeWithQualityGate(generateFn, {
|
|
|
97
97
|
const attempts = enabled ? Math.max(1, maxRerolls + 1) : 1
|
|
98
98
|
|
|
99
99
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
100
|
-
|
|
100
|
+
try {
|
|
101
|
+
lastGenerated = await generateFn(currentSeed)
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// #353: If a subsequent re-roll exceeds budget, abort further retries and preserve the initial output
|
|
104
|
+
if (lastGenerated && (err.message?.includes('budget exceeded') || err.message?.includes('Daily image generation budget'))) {
|
|
105
|
+
logger(`Quality gate re-roll halted by budget limit: ${err.message}`)
|
|
106
|
+
return {
|
|
107
|
+
generated: lastGenerated,
|
|
108
|
+
qualityReport: {
|
|
109
|
+
...lastReport,
|
|
110
|
+
rerolls,
|
|
111
|
+
exhausted: true,
|
|
112
|
+
budgetExceeded: true,
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw err
|
|
117
|
+
}
|
|
101
118
|
|
|
102
119
|
if (!enabled) {
|
|
103
120
|
return {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
} from '../providers.js'
|
|
17
17
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
18
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
18
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
19
19
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
20
20
|
import {
|
|
21
21
|
CHARACTER_SHEET_LAYOUTS,
|
|
@@ -152,15 +152,20 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
152
152
|
size: cfg.defaultSize || '1024x1024',
|
|
153
153
|
count: 1,
|
|
154
154
|
})
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
156
|
+
let gen
|
|
157
|
+
try {
|
|
158
|
+
gen = await executeWithFallback(
|
|
159
|
+
providers,
|
|
160
|
+
fallbackChain,
|
|
161
|
+
seed,
|
|
162
|
+
sheetPlan.prompt,
|
|
163
|
+
{ logger: ctx.logger },
|
|
164
|
+
)
|
|
165
|
+
} catch (err) {
|
|
166
|
+
reservation.release()
|
|
167
|
+
throw err
|
|
168
|
+
}
|
|
164
169
|
|
|
165
170
|
const usedProvider = gen._fallback?.providerUsed || provider
|
|
166
171
|
const finalBytes = gen.bytes
|
|
@@ -196,7 +201,7 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
199
|
-
|
|
204
|
+
reservation.commit(effectiveCost, {
|
|
200
205
|
ctx,
|
|
201
206
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: sheetPlan.prompt, seed },
|
|
202
207
|
})
|
package/lib/tools/diagram.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '../providers.js'
|
|
14
14
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
15
15
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
16
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
16
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
17
17
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
18
18
|
import {
|
|
19
19
|
DIAGRAM_3D_STYLES,
|
|
@@ -148,15 +148,20 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
148
148
|
size: cfg.defaultSize || '1024x1024',
|
|
149
149
|
count: 1,
|
|
150
150
|
})
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
151
|
+
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
152
|
+
let gen
|
|
153
|
+
try {
|
|
154
|
+
gen = await executeWithFallback(
|
|
155
|
+
providers,
|
|
156
|
+
fallbackChain,
|
|
157
|
+
seed,
|
|
158
|
+
plan.prompt,
|
|
159
|
+
{ logger: ctx.logger },
|
|
160
|
+
)
|
|
161
|
+
} catch (err) {
|
|
162
|
+
reservation.release()
|
|
163
|
+
throw err
|
|
164
|
+
}
|
|
160
165
|
|
|
161
166
|
const usedProvider = gen._fallback?.providerUsed || provider
|
|
162
167
|
const finalBytes = gen.bytes
|
|
@@ -192,7 +197,7 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
195
|
-
|
|
200
|
+
reservation.commit(effectiveCost, {
|
|
196
201
|
ctx,
|
|
197
202
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: plan.prompt, seed },
|
|
198
203
|
})
|
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
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '../providers.js'
|
|
16
16
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
17
17
|
import { executeWithQualityGate } from '../quality-gate.js'
|
|
18
|
-
import { calculateGenerationCost, assertBudgetAvailable } from '../cost-meter.js'
|
|
18
|
+
import { calculateGenerationCost, assertBudgetAvailable, reserveSpend } from '../cost-meter.js'
|
|
19
19
|
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
20
20
|
import { getCachedGeneration } from '../generation-cache.js'
|
|
21
21
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
@@ -378,14 +378,25 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
378
378
|
async (s, p) => {
|
|
379
379
|
let totalAttempts = 0
|
|
380
380
|
let accumulatedCost = 0
|
|
381
|
-
// #353: Pipeline evaluates raw gen.bytes
|
|
381
|
+
// #353, #355: Pipeline reserves budget per attempt, evaluates raw gen.bytes, and commits spend per attempt
|
|
382
|
+
const perAttemptCost = Number(estimatedCost / totalCount) || 0.02
|
|
382
383
|
const rawGenerate = async (seedVal) => {
|
|
384
|
+
const reservation = reserveSpend(perAttemptCost, cfg.dailyBudgetUsd)
|
|
383
385
|
totalAttempts++
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
try {
|
|
387
|
+
const gen = await providers[k](seedVal, p)
|
|
388
|
+
guard(gen)
|
|
389
|
+
const attemptCost = Number(gen.cost) || perAttemptCost
|
|
390
|
+
accumulatedCost += attemptCost
|
|
391
|
+
reservation.commit(attemptCost, {
|
|
392
|
+
ctx,
|
|
393
|
+
meta: { provider: k, model: cfg.model || cfg.customModel, prompt: p, seed: seedVal },
|
|
394
|
+
})
|
|
395
|
+
return gen
|
|
396
|
+
} catch (err) {
|
|
397
|
+
reservation.release()
|
|
398
|
+
throw err
|
|
399
|
+
}
|
|
389
400
|
}
|
|
390
401
|
|
|
391
402
|
const { generated: acceptedGen, qualityReport } = await executeWithQualityGate(
|
|
@@ -398,7 +409,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
398
409
|
}
|
|
399
410
|
)
|
|
400
411
|
|
|
401
|
-
// Save the accepted artifact ONCE
|
|
412
|
+
// Save the accepted artifact ONCE (spend already committed per attempt)
|
|
402
413
|
const saved = await saveGeneratedImageArtifact({
|
|
403
414
|
ctx,
|
|
404
415
|
exec,
|
|
@@ -417,6 +428,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
417
428
|
slugify,
|
|
418
429
|
estimatedCost: accumulatedCost || estimatedCost,
|
|
419
430
|
totalCount,
|
|
431
|
+
recordSpend: false,
|
|
420
432
|
})
|
|
421
433
|
|
|
422
434
|
return {
|
|
@@ -500,12 +512,17 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
500
512
|
const hPrompt = h
|
|
501
513
|
const found = findCachedGeneration(historyEntries, hPrompt)
|
|
502
514
|
if (found) return await cachedResult(found)
|
|
515
|
+
const requestedCriteria = {
|
|
516
|
+
format: args.output_format || cfg.defaultFormat || 'png',
|
|
517
|
+
aspectRatio: args.aspect_ratio || cfg.defaultAspectRatio,
|
|
518
|
+
size: args.aspect_ratio ? 'custom' : (args.image_size || cfg.defaultSize),
|
|
519
|
+
}
|
|
503
520
|
if (cfg.cacheBySeed) {
|
|
504
|
-
const bySeed = await findCached(historyEntries, seedVal, promptVal)
|
|
521
|
+
const bySeed = await findCached(historyEntries, seedVal, promptVal, requestedCriteria)
|
|
505
522
|
if (bySeed) return await cachedResult(bySeed)
|
|
506
523
|
}
|
|
507
524
|
if (cfg.cacheByPrompt) {
|
|
508
|
-
const byPrompt = await findCachedByPrompt(historyEntries, promptVal)
|
|
525
|
+
const byPrompt = await findCachedByPrompt(historyEntries, promptVal, requestedCriteria)
|
|
509
526
|
if (byPrompt) return await cachedResult(byPrompt)
|
|
510
527
|
}
|
|
511
528
|
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'
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
saveAttachmentSafe,
|
|
16
16
|
} from '../providers.js'
|
|
17
17
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
18
|
-
import { assertBudgetAvailable, recordSpend } from '../cost-meter.js'
|
|
18
|
+
import { assertBudgetAvailable, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
19
19
|
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
20
20
|
import { sanitizeErrorAndLogs } from '../security.js'
|
|
21
21
|
|
|
@@ -55,7 +55,7 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
55
55
|
const source = await resolveSource(ctx, exec, args.image)
|
|
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
59
|
const res = await removeBackgroundFal(
|
|
60
60
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
61
61
|
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec?.signal }
|
|
@@ -68,7 +68,7 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
68
68
|
await mkdir(outDir, { recursive: true })
|
|
69
69
|
const filePath = path.join(outDir, name)
|
|
70
70
|
await writeFile(filePath, res.bytes)
|
|
71
|
-
|
|
71
|
+
resvNobg.commit(0.02, { ctx, meta: { tool: 'remove_background', model: args.model || 'fal-ai/birefnet' } })
|
|
72
72
|
const summary = buildDualOutputMarkdown({
|
|
73
73
|
action: 'background removed',
|
|
74
74
|
filePath,
|
|
@@ -126,7 +126,7 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
126
126
|
const source = await resolveSource(ctx, exec, args.image)
|
|
127
127
|
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
128
128
|
const cfg = live()
|
|
129
|
-
|
|
129
|
+
const resvUpscale = reserveSpend(0.025, cfg.dailyBudgetUsd)
|
|
130
130
|
const res = await upscaleImageFal(
|
|
131
131
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
132
132
|
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec?.signal }
|
|
@@ -139,7 +139,7 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
139
139
|
await mkdir(outDir, { recursive: true })
|
|
140
140
|
const filePath = path.join(outDir, name)
|
|
141
141
|
await writeFile(filePath, res.bytes)
|
|
142
|
-
|
|
142
|
+
resvUpscale.commit(0.025, { ctx, meta: { tool: 'upscale_image', scale: args.scale } })
|
|
143
143
|
const summary = buildDualOutputMarkdown({
|
|
144
144
|
action: `upscaled (${args.scale || 2}x)`,
|
|
145
145
|
filePath,
|
|
@@ -252,8 +252,9 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
252
252
|
async execute(args, exec) {
|
|
253
253
|
try {
|
|
254
254
|
const cfg = live()
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
256
|
+
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
257
|
+
const resvBlend = reserveSpend(0.05, cfg.dailyBudgetUsd)
|
|
257
258
|
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
258
259
|
if (refs.length < 2) {
|
|
259
260
|
throw new Error('At least two images are required for blending')
|
|
@@ -278,7 +279,7 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
278
279
|
await mkdir(outDir, { recursive: true })
|
|
279
280
|
const filePath = path.join(outDir, name)
|
|
280
281
|
await writeFile(filePath, res.bytes)
|
|
281
|
-
|
|
282
|
+
resvBlend.commit(0.05, { ctx, meta: { tool: 'blend_images' } })
|
|
282
283
|
const summary = buildDualOutputMarkdown({
|
|
283
284
|
action: 'blended composition',
|
|
284
285
|
filePath,
|
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)
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
saveAttachmentSafe,
|
|
15
15
|
} from '../providers.js'
|
|
16
16
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
17
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
17
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
18
18
|
import {
|
|
19
19
|
normalizeMatrixStyles,
|
|
20
20
|
buildMatrixCellPrompt,
|
|
@@ -145,7 +145,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
145
145
|
size: cfg.defaultSize || '1024x1024',
|
|
146
146
|
count: cellCount,
|
|
147
147
|
})
|
|
148
|
-
|
|
148
|
+
const batchReservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
149
149
|
|
|
150
150
|
const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
151
151
|
const quadrantIds = ['A', 'B', 'C', 'D']
|
|
@@ -187,7 +187,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
187
187
|
})
|
|
188
188
|
|
|
189
189
|
const cellCost = Number(gen.cost) || +(estimatedCost / cellCount).toFixed(4)
|
|
190
|
-
|
|
190
|
+
batchReservation.commit(cellCost, {
|
|
191
191
|
ctx,
|
|
192
192
|
meta: { provider, model: cfg.model || cfg.customModel, prompt: cellPrompt, seed: cellSeed },
|
|
193
193
|
})
|
package/lib/tools/theme-pair.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
saveAttachmentSafe,
|
|
14
14
|
} from '../providers.js'
|
|
15
15
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
16
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
16
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
17
17
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
18
18
|
import {
|
|
19
19
|
THEME_STYLES,
|
|
@@ -153,7 +153,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
153
153
|
size: cfg.defaultSize || '1024x1024',
|
|
154
154
|
count: 2,
|
|
155
155
|
})
|
|
156
|
-
|
|
156
|
+
const batchReservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
157
157
|
|
|
158
158
|
const chain = resolveFallbackChain(primaryProvider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
159
159
|
const baseSeed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 1000000)
|
|
@@ -202,7 +202,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
202
202
|
height: lightGen.height || aspectPixels[1],
|
|
203
203
|
})
|
|
204
204
|
const halfCost = +(estimatedCost / 2).toFixed(4)
|
|
205
|
-
|
|
205
|
+
batchReservation.commit(Number(lightGen.cost) || halfCost, {
|
|
206
206
|
ctx,
|
|
207
207
|
meta: { provider: primaryProvider, model: cfg.model || cfg.customModel, prompt: lightPrompt },
|
|
208
208
|
})
|
|
@@ -245,7 +245,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
245
245
|
width: darkGen.width || aspectPixels[0],
|
|
246
246
|
height: darkGen.height || aspectPixels[1],
|
|
247
247
|
})
|
|
248
|
-
|
|
248
|
+
batchReservation.commit(Number(darkGen.cost) || halfCost, {
|
|
249
249
|
ctx,
|
|
250
250
|
meta: { provider: primaryProvider, model: cfg.model || cfg.customModel, prompt: darkPrompt },
|
|
251
251
|
})
|
package/lib/tools/ui-asset.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
saveAttachmentSafe,
|
|
16
16
|
} from '../providers.js'
|
|
17
17
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
18
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
18
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
19
19
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
20
20
|
import {
|
|
21
21
|
UI_ASSET_TYPES,
|
|
@@ -168,14 +168,19 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
168
168
|
size: cfg.defaultSize || '1024x1024',
|
|
169
169
|
count: 1,
|
|
170
170
|
})
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
173
172
|
const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
174
173
|
const seedVal = args.seed ?? Math.floor(Math.random() * 100000)
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
let gen
|
|
176
|
+
try {
|
|
177
|
+
gen = await executeWithFallback(providers, chain, seedVal, synthesizedPrompt, {
|
|
178
|
+
logger: ctx.logger,
|
|
179
|
+
})
|
|
180
|
+
} catch (err) {
|
|
181
|
+
reservation.release()
|
|
182
|
+
throw err
|
|
183
|
+
}
|
|
179
184
|
|
|
180
185
|
let finalBytes = gen.bytes
|
|
181
186
|
let mediaType = gen.mediaType || 'image/png'
|
|
@@ -211,7 +216,7 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
211
216
|
})
|
|
212
217
|
|
|
213
218
|
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
214
|
-
|
|
219
|
+
reservation.commit(effectiveCost, {
|
|
215
220
|
ctx,
|
|
216
221
|
meta: { provider, model: cfg.model || cfg.customModel, prompt: synthesizedPrompt, seed: seedVal },
|
|
217
222
|
})
|
package/lib/tools/vision-ocr.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from '../providers.js'
|
|
13
13
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
14
14
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
15
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
15
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
16
16
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
17
17
|
import {
|
|
18
18
|
normalizeBbox,
|
|
@@ -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) {
|
|
@@ -241,15 +252,20 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
241
252
|
size: `${imgWidth}x${imgHeight}`,
|
|
242
253
|
count: 1,
|
|
243
254
|
})
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
256
|
+
let gen
|
|
257
|
+
try {
|
|
258
|
+
gen = await executeWithFallback(
|
|
259
|
+
providers,
|
|
260
|
+
fallbackChain,
|
|
261
|
+
seed,
|
|
262
|
+
inpaintPrompt,
|
|
263
|
+
{ logger: ctx.logger },
|
|
264
|
+
)
|
|
265
|
+
} catch (err) {
|
|
266
|
+
reservation.release()
|
|
267
|
+
throw err
|
|
268
|
+
}
|
|
253
269
|
|
|
254
270
|
const inpaintedBytes = gen.bytes
|
|
255
271
|
if (!inpaintedBytes) {
|
|
@@ -258,13 +274,23 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
258
274
|
|
|
259
275
|
// 5. Composite typography overlay on inpainted background
|
|
260
276
|
let finalBytes = inpaintedBytes
|
|
277
|
+
let composited = false
|
|
261
278
|
try {
|
|
262
279
|
const sharpMod = await import('sharp')
|
|
263
280
|
const sharp = sharpMod.default || sharpMod
|
|
281
|
+
const inpaintMeta = await sharp(inpaintedBytes).metadata()
|
|
282
|
+
const compW = inpaintMeta.width || imgWidth
|
|
283
|
+
const compH = inpaintMeta.height || imgHeight
|
|
284
|
+
let compOverlay = overlayBuffer
|
|
285
|
+
if (compW !== imgWidth || compH !== imgHeight) {
|
|
286
|
+
const scaledOverlaySvg = buildTextOverlaySvg(compW, compH, overlayItems, args.font_family || 'sans-serif')
|
|
287
|
+
compOverlay = Buffer.from(scaledOverlaySvg, 'utf8')
|
|
288
|
+
}
|
|
264
289
|
finalBytes = await sharp(inpaintedBytes)
|
|
265
|
-
.composite([{ input:
|
|
290
|
+
.composite([{ input: compOverlay, top: 0, left: 0 }])
|
|
266
291
|
.png()
|
|
267
292
|
.toBuffer()
|
|
293
|
+
composited = true
|
|
268
294
|
} catch (sharpErr) {
|
|
269
295
|
if (ctx.logger?.warn) ctx.logger.warn(`Sharp compositing skipped: ${sharpErr.message}`)
|
|
270
296
|
}
|
|
@@ -302,7 +328,7 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
302
328
|
})
|
|
303
329
|
|
|
304
330
|
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
305
|
-
|
|
331
|
+
reservation.commit(effectiveCost, {
|
|
306
332
|
ctx,
|
|
307
333
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: inpaintPrompt, seed },
|
|
308
334
|
})
|
|
@@ -322,7 +348,7 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
322
348
|
|
|
323
349
|
const response = {
|
|
324
350
|
summary: dualOutput,
|
|
325
|
-
replacedCount: overlayItems.length,
|
|
351
|
+
replacedCount: composited ? overlayItems.length : 0,
|
|
326
352
|
bboxes,
|
|
327
353
|
inpaintPrompt,
|
|
328
354
|
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.31",
|
|
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",
|