@goodandready/dsh-image-gen 0.11.27 → 0.11.29
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/comfy-workflow-helpers.js +14 -0
- package/lib/generation-helpers.js +20 -3
- package/lib/providers/backends/custom.js +7 -1
- package/lib/providers/backends/fal.js +18 -0
- package/lib/providers/backends/gemini.js +12 -0
- package/lib/providers/backends/local.js +11 -0
- package/lib/providers/backends/replicate.js +5 -0
- package/lib/providers/backends/seedream.js +3 -0
- package/lib/providers/backends/subscription.js +3 -0
- package/lib/providers.js +142 -71
- package/lib/register-tools.js +6 -4
- package/lib/tools/anchor.js +10 -3
- package/lib/tools/character-sheet.js +4 -0
- package/lib/tools/diagram.js +4 -0
- package/lib/tools/generation-pack.js +4 -1
- package/lib/tools/generation.js +25 -3
- package/lib/tools/processing-basic.js +7 -7
- package/lib/tools/style-matrix.js +2 -2
- package/lib/tools/theme-pair.js +3 -3
- package/lib/tools/ui-asset.js +3 -3
- package/lib/tools/vision-ocr.js +4 -0
- package/package.json +6 -2
|
@@ -73,6 +73,10 @@ export function interpolateComfyWorkflow(workflowGraph, ctx) {
|
|
|
73
73
|
const cfgVal = Number.isFinite(ctx.cfg) ? ctx.cfg : 7
|
|
74
74
|
const modelVal = String(ctx.model ?? 'v1-5-pruned-emaonly.safetensors')
|
|
75
75
|
const imageVal = String(ctx.image ?? '')
|
|
76
|
+
const faceImageVal = String(ctx.faceImage ?? ctx.face_image ?? '')
|
|
77
|
+
const faceWeightVal = Number.isFinite(ctx.faceWeight ?? ctx.face_weight) ? (ctx.faceWeight ?? ctx.face_weight) : 0.75
|
|
78
|
+
const refImageVal = String(ctx.referenceImage ?? ctx.reference_image ?? '')
|
|
79
|
+
const refWeightVal = Number.isFinite(ctx.referenceWeight ?? ctx.reference_weight) ? (ctx.referenceWeight ?? ctx.reference_weight) : 0.65
|
|
76
80
|
|
|
77
81
|
function transformValue(val) {
|
|
78
82
|
if (typeof val === 'string') {
|
|
@@ -83,6 +87,8 @@ export function interpolateComfyWorkflow(workflowGraph, ctx) {
|
|
|
83
87
|
if (trimmed === '{{height}}') return heightVal
|
|
84
88
|
if (trimmed === '{{steps}}') return stepsVal
|
|
85
89
|
if (trimmed === '{{cfg}}') return cfgVal
|
|
90
|
+
if (trimmed === '{{face_weight}}' || trimmed === '{{faceWeight}}') return faceWeightVal
|
|
91
|
+
if (trimmed === '{{reference_weight}}' || trimmed === '{{referenceWeight}}') return refWeightVal
|
|
86
92
|
|
|
87
93
|
// General string replacements
|
|
88
94
|
return val
|
|
@@ -96,6 +102,14 @@ export function interpolateComfyWorkflow(workflowGraph, ctx) {
|
|
|
96
102
|
.replace(/\{\{cfg\}\}/g, String(cfgVal))
|
|
97
103
|
.replace(/\{\{model\}\}/g, modelVal)
|
|
98
104
|
.replace(/\{\{image\}\}/g, imageVal)
|
|
105
|
+
.replace(/\{\{face_image\}\}/g, faceImageVal)
|
|
106
|
+
.replace(/\{\{faceImage\}\}/g, faceImageVal)
|
|
107
|
+
.replace(/\{\{face_weight\}\}/g, String(faceWeightVal))
|
|
108
|
+
.replace(/\{\{faceWeight\}\}/g, String(faceWeightVal))
|
|
109
|
+
.replace(/\{\{reference_image\}\}/g, refImageVal)
|
|
110
|
+
.replace(/\{\{referenceImage\}\}/g, refImageVal)
|
|
111
|
+
.replace(/\{\{reference_weight\}\}/g, String(refWeightVal))
|
|
112
|
+
.replace(/\{\{referenceWeight\}\}/g, String(refWeightVal))
|
|
99
113
|
}
|
|
100
114
|
if (Array.isArray(val)) {
|
|
101
115
|
return val.map(transformValue)
|
|
@@ -72,8 +72,11 @@ export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId,
|
|
|
72
72
|
let effectiveNegative = styleInfo.negativePrompt
|
|
73
73
|
const activeAnchor = getSessionAnchor(sessionId)
|
|
74
74
|
let resolvedFaceRef
|
|
75
|
+
let resolvedRefImage
|
|
75
76
|
|
|
76
77
|
const faceRefInput = args.face_reference || (activeAnchor && (activeAnchor.mode === 'face' || activeAnchor.mode === 'character') ? activeAnchor.image : undefined)
|
|
78
|
+
const refImageInput = args.reference_image || (activeAnchor && activeAnchor.mode === 'style' ? activeAnchor.image : undefined)
|
|
79
|
+
|
|
77
80
|
if (faceRefInput) {
|
|
78
81
|
try {
|
|
79
82
|
resolvedFaceRef = await resolveSource(ctx, exec, faceRefInput)
|
|
@@ -90,9 +93,22 @@ export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId,
|
|
|
90
93
|
if (facePlan.negativePrompt) {
|
|
91
94
|
effectiveNegative = effectiveNegative ? `${effectiveNegative}, ${facePlan.negativePrompt}` : facePlan.negativePrompt
|
|
92
95
|
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (refImageInput) {
|
|
99
|
+
try {
|
|
100
|
+
resolvedRefImage = await resolveSource(ctx, exec, refImageInput)
|
|
101
|
+
} catch (_err) {
|
|
102
|
+
resolvedRefImage = { url: refImageInput }
|
|
103
|
+
}
|
|
104
|
+
if (!faceRefInput) {
|
|
105
|
+
if (activeAnchor) {
|
|
106
|
+
effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
|
|
107
|
+
} else {
|
|
108
|
+
effectivePrompt = `${effectivePrompt}, visual reference anchor`
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else if (!faceRefInput && activeAnchor) {
|
|
96
112
|
effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -113,6 +129,7 @@ export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId,
|
|
|
113
129
|
styleInfo,
|
|
114
130
|
activeAnchor,
|
|
115
131
|
resolvedFaceRef,
|
|
132
|
+
resolvedRefImage,
|
|
116
133
|
}
|
|
117
134
|
}
|
|
118
135
|
|
|
@@ -19,12 +19,18 @@ export function createCustomGenerator(deps, job) {
|
|
|
19
19
|
const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels, aspectRatio } = job
|
|
20
20
|
|
|
21
21
|
async function custom(seedArg = seed, promptArg = prompt) {
|
|
22
|
+
if (job.faceReference && !source) {
|
|
23
|
+
throw new Error('Custom provider (OpenAI-compatible) does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter/InstantID) or FAL')
|
|
24
|
+
}
|
|
22
25
|
const base = String(cfg.customBaseURL || '').replace(/\/+$/, '')
|
|
23
|
-
if (!base) throw new Error('Custom image provider: base URL is not configured (Settings
|
|
26
|
+
if (!base) throw new Error('Custom image provider: base URL is not configured (Settings в†’ Image generation)')
|
|
24
27
|
if (!cfg.customModel) throw new Error('Custom image provider: model is not configured')
|
|
25
28
|
|
|
26
29
|
// Empty key reference indicates unauthenticated gateway.
|
|
27
30
|
const key = cfg.customKeyEnv ? await resolveKey(cfg.customKeyEnv) : ''
|
|
31
|
+
if (job.faceReference && !source) {
|
|
32
|
+
throw new Error('Custom provider (OpenAI-compatible) does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter/InstantID) or FAL')
|
|
33
|
+
}
|
|
28
34
|
const isEdit = Boolean(source && source.bytes)
|
|
29
35
|
const headers = {}
|
|
30
36
|
if (!isEdit) {
|
|
@@ -34,6 +34,24 @@ export function createFalGenerator(deps, job) {
|
|
|
34
34
|
if (strength !== undefined) {
|
|
35
35
|
body.strength = strength
|
|
36
36
|
}
|
|
37
|
+
} else if (job.faceReference && (job.faceReference.bytes || job.faceReference.url)) {
|
|
38
|
+
const refDataUri = job.faceReference.bytes
|
|
39
|
+
? `data:${job.faceReference.mediaType || 'image/png'};base64,${Buffer.from(job.faceReference.bytes).toString('base64')}`
|
|
40
|
+
: job.faceReference.url
|
|
41
|
+
body.reference_image_url = refDataUri
|
|
42
|
+
body.face_image_url = refDataUri
|
|
43
|
+
if (job.faceStrength !== undefined) {
|
|
44
|
+
body.id_weight = job.faceStrength
|
|
45
|
+
body.reference_weight = job.faceStrength
|
|
46
|
+
}
|
|
47
|
+
} else if (job.referenceImage && (job.referenceImage.bytes || job.referenceImage.url)) {
|
|
48
|
+
const refDataUri = job.referenceImage.bytes
|
|
49
|
+
? `data:${job.referenceImage.mediaType || 'image/png'};base64,${Buffer.from(job.referenceImage.bytes).toString('base64')}`
|
|
50
|
+
: job.referenceImage.url
|
|
51
|
+
body.reference_image_url = refDataUri
|
|
52
|
+
if (job.referenceStrength !== undefined) {
|
|
53
|
+
body.reference_weight = job.referenceStrength
|
|
54
|
+
}
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
if (typeof onProgress === 'function') {
|
|
@@ -38,6 +38,18 @@ export function createGeminiGenerator(deps, job) {
|
|
|
38
38
|
data: sourceBase64,
|
|
39
39
|
},
|
|
40
40
|
})
|
|
41
|
+
} else {
|
|
42
|
+
const refTarget = job.faceReference || job.referenceImage
|
|
43
|
+
if (refTarget && refTarget.bytes) {
|
|
44
|
+
const refMime = refTarget.mediaType || 'image/png'
|
|
45
|
+
const refBase64 = Buffer.isBuffer(refTarget.bytes) ? refTarget.bytes.toString('base64') : Buffer.from(refTarget.bytes).toString('base64')
|
|
46
|
+
parts.push({
|
|
47
|
+
inlineData: {
|
|
48
|
+
mimeType: refMime,
|
|
49
|
+
data: refBase64,
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
41
53
|
}
|
|
42
54
|
parts.push({ text: promptArg })
|
|
43
55
|
|
|
@@ -92,6 +92,13 @@ export function createLocalGenerator(deps, job) {
|
|
|
92
92
|
let workflowGraph
|
|
93
93
|
if (cfg.comfyWorkflowJson) {
|
|
94
94
|
const template = validateComfyWorkflow(cfg.comfyWorkflowJson)
|
|
95
|
+
const faceData = job.faceReference && job.faceReference.bytes
|
|
96
|
+
? Buffer.from(job.faceReference.bytes).toString('base64')
|
|
97
|
+
: (job.faceReference?.url || '')
|
|
98
|
+
const refData = job.referenceImage && job.referenceImage.bytes
|
|
99
|
+
? Buffer.from(job.referenceImage.bytes).toString('base64')
|
|
100
|
+
: (job.referenceImage?.url || '')
|
|
101
|
+
|
|
95
102
|
workflowGraph = interpolateComfyWorkflow(template, {
|
|
96
103
|
prompt: promptArg,
|
|
97
104
|
negativePrompt: negativePrompt || '',
|
|
@@ -102,6 +109,10 @@ export function createLocalGenerator(deps, job) {
|
|
|
102
109
|
cfg: cfg.localCfg ?? 7,
|
|
103
110
|
model: cfg.localModel,
|
|
104
111
|
image: source && source.bytes ? Buffer.from(source.bytes).toString('base64') : '',
|
|
112
|
+
faceImage: faceData,
|
|
113
|
+
faceWeight: job.faceStrength ?? 0.75,
|
|
114
|
+
referenceImage: refData,
|
|
115
|
+
referenceWeight: job.referenceStrength ?? 0.65,
|
|
105
116
|
})
|
|
106
117
|
} else {
|
|
107
118
|
workflowGraph = buildDefaultComfyWorkflow({
|
|
@@ -31,6 +31,11 @@ export function createReplicateGenerator(deps, job) {
|
|
|
31
31
|
image: `data:${source.mediaType || 'image/png'};base64,${Buffer.from(source.bytes).toString('base64')}`,
|
|
32
32
|
...(mask && mask.bytes ? { mask: `data:${mask.mediaType || 'image/png'};base64,${Buffer.from(mask.bytes).toString('base64')}` } : {}),
|
|
33
33
|
...(strength !== undefined ? { prompt_strength: strength } : {}),
|
|
34
|
+
} : (job.faceReference || job.referenceImage) ? {
|
|
35
|
+
face_image: (job.faceReference || job.referenceImage).bytes
|
|
36
|
+
? `data:${(job.faceReference || job.referenceImage).mediaType || 'image/png'};base64,${Buffer.from((job.faceReference || job.referenceImage).bytes).toString('base64')}`
|
|
37
|
+
: (job.faceReference || job.referenceImage).url,
|
|
38
|
+
id_weight: job.faceStrength ?? 0.75,
|
|
34
39
|
} : {}),
|
|
35
40
|
},
|
|
36
41
|
}
|
|
@@ -14,6 +14,9 @@ export function createSeedreamGenerator(deps, job) {
|
|
|
14
14
|
const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels, aspectRatio } = job
|
|
15
15
|
|
|
16
16
|
async function seedream(seedArg = seed, promptArg = prompt) {
|
|
17
|
+
if (job.faceReference) {
|
|
18
|
+
throw new Error('Seedream does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter/InstantID) or FAL')
|
|
19
|
+
}
|
|
17
20
|
const key = await resolveKey(cfg.seedreamKeyEnv)
|
|
18
21
|
const base = (cfg.seedreamBaseURL || 'https://api.bytedanceapi.com/v1').replace(/\/+$/, '')
|
|
19
22
|
const timeoutSignal = typeof AbortSignal !== 'undefined' && AbortSignal.timeout
|
|
@@ -18,6 +18,9 @@ export function createSubscriptionGenerator(deps, job) {
|
|
|
18
18
|
if (source) {
|
|
19
19
|
return { ok: false, provider, reason: `${provider}: does not support image editing — use fal, custom, or local` }
|
|
20
20
|
}
|
|
21
|
+
if (job.faceReference) {
|
|
22
|
+
return { ok: false, provider, reason: `${provider}: does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter) or FAL` }
|
|
23
|
+
}
|
|
21
24
|
const images = deps.subscriptionImages
|
|
22
25
|
if (!images || typeof images.generate !== 'function') {
|
|
23
26
|
return {
|
package/lib/providers.js
CHANGED
|
@@ -640,7 +640,7 @@ export function toLosslessJson(val) {
|
|
|
640
640
|
* Returns { ok: boolean, latencyMs: number, message: string }.
|
|
641
641
|
*/
|
|
642
642
|
export async function testProviderConnection(deps, providerId) {
|
|
643
|
-
const { fetchImpl = fetch, resolveKey, cfg } = deps
|
|
643
|
+
const { fetchImpl = fetch, resolveKey, cfg } = deps || {}
|
|
644
644
|
const start = Date.now()
|
|
645
645
|
const rawProvider = (typeof providerId === 'string' && providerId.trim())
|
|
646
646
|
|| (typeof cfg?.provider === 'string' && cfg.provider.trim())
|
|
@@ -648,128 +648,199 @@ export async function testProviderConnection(deps, providerId) {
|
|
|
648
648
|
|| 'fal'
|
|
649
649
|
const p = rawProvider.toLowerCase()
|
|
650
650
|
|
|
651
|
+
const safeTimeout = (ms) => {
|
|
652
|
+
return typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(ms) : undefined
|
|
653
|
+
}
|
|
654
|
+
|
|
651
655
|
try {
|
|
652
656
|
if (p === 'fal') {
|
|
653
|
-
const key = await resolveKey(cfg
|
|
654
|
-
if (!key) return { ok: false, latencyMs: 0, message: 'FAL API key not configured' }
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
657
|
+
const key = await resolveKey(cfg?.apiKeyEnv || 'FAL_KEY')
|
|
658
|
+
if (!key) return { ok: false, state: 'not_configured', latencyMs: 0, message: 'FAL API key not configured' }
|
|
659
|
+
let res
|
|
660
|
+
try {
|
|
661
|
+
res = await fetchImpl('https://queue.fal.run/fal-ai/fast-sdxl/status', {
|
|
662
|
+
headers: { Authorization: falAuthHeader(key) },
|
|
663
|
+
signal: safeTimeout(6000),
|
|
664
|
+
})
|
|
665
|
+
} catch (netErr) {
|
|
666
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `Fal queue unreachable: ${netErr.message || 'connection error'}` }
|
|
667
|
+
}
|
|
659
668
|
const latencyMs = Date.now() - start
|
|
660
669
|
if (res.status === 401 || res.status === 403) {
|
|
661
|
-
return { ok: false, latencyMs, message: `Fal auth failed (HTTP ${res.status})` }
|
|
670
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `Fal auth failed (HTTP ${res.status})` }
|
|
671
|
+
}
|
|
672
|
+
if (res.status >= 500) {
|
|
673
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Fal server error (HTTP ${res.status})` }
|
|
662
674
|
}
|
|
663
|
-
return { ok: true, latencyMs, message: 'Fal.ai queue reachable' }
|
|
675
|
+
return { ok: true, state: 'authenticated', latencyMs, message: 'Fal.ai queue reachable' }
|
|
664
676
|
}
|
|
665
677
|
|
|
666
|
-
if (p === 'comfyui') {
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
678
|
+
if (p === 'local' || p === 'comfyui' || p === 'a1111') {
|
|
679
|
+
const backend = (p === 'local' ? (cfg?.localBackend || 'comfyui') : p).toLowerCase()
|
|
680
|
+
if (backend === 'a1111') {
|
|
681
|
+
const url = cfg?.a1111Url || cfg?.localBaseURL || 'http://127.0.0.1:7860'
|
|
682
|
+
let res
|
|
683
|
+
try {
|
|
684
|
+
res = await fetchImpl(`${url.replace(/\/+$/, '')}/sdapi/v1/samplers`, {
|
|
685
|
+
signal: safeTimeout(5000),
|
|
686
|
+
})
|
|
687
|
+
} catch (netErr) {
|
|
688
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `Automatic1111 offline (${netErr.message || 'connection refused'})` }
|
|
689
|
+
}
|
|
690
|
+
const latencyMs = Date.now() - start
|
|
691
|
+
if (res.status === 401 || res.status === 403) {
|
|
692
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `A1111 auth error (HTTP ${res.status})` }
|
|
693
|
+
}
|
|
694
|
+
if (res.status >= 500) {
|
|
695
|
+
return { ok: false, state: 'server_error', latencyMs, message: `A1111 server error (HTTP ${res.status})` }
|
|
696
|
+
}
|
|
697
|
+
if (res.ok) {
|
|
698
|
+
return { ok: true, state: 'model_available', latencyMs, message: 'Automatic1111 operational' }
|
|
699
|
+
}
|
|
700
|
+
return { ok: false, state: 'server_error', latencyMs, message: `A1111 returned HTTP ${res.status}` }
|
|
676
701
|
}
|
|
677
|
-
return { ok: false, latencyMs, message: `ComfyUI returned HTTP ${res.status}` }
|
|
678
|
-
}
|
|
679
702
|
|
|
680
|
-
|
|
681
|
-
const url = cfg
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
703
|
+
// Default local is ComfyUI
|
|
704
|
+
const url = cfg?.comfyuiUrl || cfg?.localBaseURL || 'http://127.0.0.1:8188'
|
|
705
|
+
let res
|
|
706
|
+
try {
|
|
707
|
+
res = await fetchImpl(`${url.replace(/\/+$/, '')}/system_stats`, {
|
|
708
|
+
signal: safeTimeout(5000),
|
|
709
|
+
})
|
|
710
|
+
} catch (netErr) {
|
|
711
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `ComfyUI offline (${netErr.message || 'connection refused'})` }
|
|
712
|
+
}
|
|
685
713
|
const latencyMs = Date.now() - start
|
|
714
|
+
if (res.status === 401 || res.status === 403) {
|
|
715
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `ComfyUI auth error (HTTP ${res.status})` }
|
|
716
|
+
}
|
|
717
|
+
if (res.status >= 500) {
|
|
718
|
+
return { ok: false, state: 'server_error', latencyMs, message: `ComfyUI server error (HTTP ${res.status})` }
|
|
719
|
+
}
|
|
686
720
|
if (res.ok) {
|
|
687
|
-
|
|
721
|
+
const data = await res.json().catch(() => ({}))
|
|
722
|
+
const vram = data?.devices?.[0]?.vram_free ? ` (${Math.round(data.devices[0].vram_free / 1048576)} MB free VRAM)` : ''
|
|
723
|
+
return { ok: true, state: 'model_available', latencyMs, message: `ComfyUI operational${vram}` }
|
|
688
724
|
}
|
|
689
|
-
return { ok: false, latencyMs, message: `
|
|
725
|
+
return { ok: false, state: 'server_error', latencyMs, message: `ComfyUI returned HTTP ${res.status}` }
|
|
690
726
|
}
|
|
691
727
|
|
|
692
728
|
if (p === 'custom') {
|
|
693
|
-
const url = cfg
|
|
694
|
-
const key = await resolveKey(cfg
|
|
729
|
+
const url = cfg?.customEndpoint || cfg?.customBaseURL || 'https://api.openai.com/v1'
|
|
730
|
+
const key = await resolveKey(cfg?.customApiKeyEnv || cfg?.customKeyEnv || 'OPENAI_API_KEY')
|
|
695
731
|
const headers = key ? { Authorization: `Bearer ${key}` } : {}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
732
|
+
let res
|
|
733
|
+
try {
|
|
734
|
+
res = await fetchImpl(`${url.replace(/\/+$/, '')}/models`, {
|
|
735
|
+
headers,
|
|
736
|
+
signal: safeTimeout(6000),
|
|
737
|
+
})
|
|
738
|
+
} catch (netErr) {
|
|
739
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `Custom endpoint offline (${netErr.message || 'connection error'})` }
|
|
740
|
+
}
|
|
700
741
|
const latencyMs = Date.now() - start
|
|
701
742
|
if (res.status === 401 || res.status === 403) {
|
|
702
|
-
return { ok: false, latencyMs, message: `Custom endpoint auth error (HTTP ${res.status})` }
|
|
743
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `Custom endpoint auth error (HTTP ${res.status})` }
|
|
744
|
+
}
|
|
745
|
+
if (res.status >= 500) {
|
|
746
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Custom endpoint server error (HTTP ${res.status})` }
|
|
703
747
|
}
|
|
704
748
|
if (res.ok || res.status === 404 || res.status === 405) {
|
|
705
|
-
return { ok: true, latencyMs, message: `Custom gateway reachable (HTTP ${res.status})` }
|
|
749
|
+
return { ok: true, state: res.ok ? 'authenticated' : 'reachable', latencyMs, message: `Custom gateway reachable (HTTP ${res.status})` }
|
|
706
750
|
}
|
|
707
|
-
return { ok: false, latencyMs, message: `Custom endpoint returned HTTP ${res.status}` }
|
|
751
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Custom endpoint returned HTTP ${res.status}` }
|
|
708
752
|
}
|
|
709
753
|
|
|
710
754
|
if (p === 'subscription' || p === 'chatgpt' || p === 'grok' || p === 'codex') {
|
|
711
|
-
const
|
|
755
|
+
const subService = deps?.subscriptionImages
|
|
756
|
+
|| (deps?.ctx?.get && deps.ctx.get('subscriptionImages'))
|
|
757
|
+
|| deps?.ctx?.subscriptionImages
|
|
758
|
+
const hasSub = Boolean(subService && typeof subService.generate === 'function')
|
|
712
759
|
const latencyMs = Date.now() - start
|
|
713
760
|
if (hasSub) {
|
|
714
|
-
return { ok: true, latencyMs, message: 'Subscription bridge available' }
|
|
761
|
+
return { ok: true, state: 'authenticated', latencyMs, message: 'Subscription bridge available' }
|
|
715
762
|
}
|
|
716
|
-
return { ok: false, latencyMs, message: 'dsh-subscriptions plugin
|
|
763
|
+
return { ok: false, state: 'not_configured', latencyMs, message: 'subscriptionImages service not available (requires dsh-subscriptions plugin)' }
|
|
717
764
|
}
|
|
718
765
|
|
|
719
766
|
if (p === 'replicate') {
|
|
720
|
-
const key = await resolveKey(cfg
|
|
721
|
-
if (!key) return { ok: false, latencyMs: 0, message: 'Replicate API token not configured' }
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
767
|
+
const key = await resolveKey(cfg?.replicateKeyEnv || 'REPLICATE_API_TOKEN')
|
|
768
|
+
if (!key) return { ok: false, state: 'not_configured', latencyMs: 0, message: 'Replicate API token not configured' }
|
|
769
|
+
let res
|
|
770
|
+
try {
|
|
771
|
+
res = await fetchImpl('https://api.replicate.com/v1/models', {
|
|
772
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
773
|
+
signal: safeTimeout(6000),
|
|
774
|
+
})
|
|
775
|
+
} catch (netErr) {
|
|
776
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `Replicate unreachable (${netErr.message || 'connection error'})` }
|
|
777
|
+
}
|
|
726
778
|
const latencyMs = Date.now() - start
|
|
727
779
|
if (res.status === 401 || res.status === 403) {
|
|
728
|
-
return { ok: false, latencyMs, message: `Replicate auth error (HTTP ${res.status})` }
|
|
780
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `Replicate auth error (HTTP ${res.status})` }
|
|
781
|
+
}
|
|
782
|
+
if (res.status >= 500) {
|
|
783
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Replicate server error (HTTP ${res.status})` }
|
|
729
784
|
}
|
|
730
785
|
if (res.ok) {
|
|
731
|
-
return { ok: true, latencyMs, message: 'Replicate API reachable' }
|
|
786
|
+
return { ok: true, state: 'authenticated', latencyMs, message: 'Replicate API reachable' }
|
|
732
787
|
}
|
|
733
|
-
return { ok: false, latencyMs, message: `Replicate returned HTTP ${res.status}` }
|
|
788
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Replicate returned HTTP ${res.status}` }
|
|
734
789
|
}
|
|
735
790
|
|
|
736
791
|
if (p === 'gemini') {
|
|
737
|
-
const key = await resolveKey(cfg
|
|
738
|
-
if (!key) return { ok: false, latencyMs: 0, message: 'Google Gemini API key not configured' }
|
|
739
|
-
const model = cfg
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
792
|
+
const key = await resolveKey(cfg?.geminiKeyEnv || 'GEMINI_API_KEY')
|
|
793
|
+
if (!key) return { ok: false, state: 'not_configured', latencyMs: 0, message: 'Google Gemini API key not configured' }
|
|
794
|
+
const model = cfg?.geminiModel || 'gemini-2.0-flash-exp-image-generation'
|
|
795
|
+
let res
|
|
796
|
+
try {
|
|
797
|
+
res = await fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${model}?key=${encodeURIComponent(key)}`, {
|
|
798
|
+
signal: safeTimeout(6000),
|
|
799
|
+
})
|
|
800
|
+
} catch (netErr) {
|
|
801
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `Google Gemini API unreachable (${netErr.message || 'connection error'})` }
|
|
802
|
+
}
|
|
743
803
|
const latencyMs = Date.now() - start
|
|
744
804
|
if (res.status === 400 || res.status === 401 || res.status === 403) {
|
|
745
|
-
return { ok: false, latencyMs, message: `Gemini API auth error (HTTP ${res.status})` }
|
|
805
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `Gemini API auth error (HTTP ${res.status})` }
|
|
806
|
+
}
|
|
807
|
+
if (res.status >= 500) {
|
|
808
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Gemini API server error (HTTP ${res.status})` }
|
|
746
809
|
}
|
|
747
810
|
if (res.ok) {
|
|
748
|
-
return { ok: true, latencyMs, message: 'Gemini Vision API operational' }
|
|
811
|
+
return { ok: true, state: 'model_available', latencyMs, message: 'Gemini Vision API operational' }
|
|
749
812
|
}
|
|
750
|
-
return { ok: false, latencyMs, message: `Gemini returned HTTP ${res.status}` }
|
|
813
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Gemini returned HTTP ${res.status}` }
|
|
751
814
|
}
|
|
752
815
|
|
|
753
816
|
if (p === 'seedream') {
|
|
754
|
-
const url = cfg
|
|
755
|
-
const key = await resolveKey(cfg
|
|
817
|
+
const url = cfg?.seedreamBaseURL || 'https://api.bytedanceapi.com/v1'
|
|
818
|
+
const key = await resolveKey(cfg?.seedreamKeyEnv || 'SEEDREAM_API_KEY')
|
|
756
819
|
const headers = key ? { Authorization: `Bearer ${key}` } : {}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
820
|
+
let res
|
|
821
|
+
try {
|
|
822
|
+
res = await fetchImpl(`${url.replace(/\/+$/, '')}/models`, {
|
|
823
|
+
headers,
|
|
824
|
+
signal: safeTimeout(6000),
|
|
825
|
+
})
|
|
826
|
+
} catch (netErr) {
|
|
827
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: `Seedream endpoint offline (${netErr.message || 'connection error'})` }
|
|
828
|
+
}
|
|
761
829
|
const latencyMs = Date.now() - start
|
|
762
|
-
if (res
|
|
763
|
-
return { ok:
|
|
830
|
+
if (res.status === 401 || res.status === 403) {
|
|
831
|
+
return { ok: false, state: 'unauthorized', latencyMs, message: `Seedream auth error (HTTP ${res.status})` }
|
|
764
832
|
}
|
|
765
|
-
if (res
|
|
766
|
-
return { ok: false, latencyMs, message: `Seedream
|
|
833
|
+
if (res.status >= 500) {
|
|
834
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Seedream server error (HTTP ${res.status})` }
|
|
835
|
+
}
|
|
836
|
+
if (res.ok || res.status === 404 || res.status === 405) {
|
|
837
|
+
return { ok: true, state: res.ok ? 'authenticated' : 'reachable', latencyMs, message: 'Seedream endpoint reachable' }
|
|
767
838
|
}
|
|
768
|
-
return { ok:
|
|
839
|
+
return { ok: false, state: 'server_error', latencyMs, message: `Seedream returned HTTP ${res.status}` }
|
|
769
840
|
}
|
|
770
841
|
|
|
771
|
-
return { ok:
|
|
842
|
+
return { ok: false, state: 'unsupported', latencyMs: Date.now() - start, message: `Unknown or unsupported provider: "${p}"` }
|
|
772
843
|
} catch (err) {
|
|
773
|
-
return { ok: false, latencyMs: Date.now() - start, message: err.message || 'Connection timeout' }
|
|
844
|
+
return { ok: false, state: 'offline', latencyMs: Date.now() - start, message: err.message || 'Connection timeout' }
|
|
774
845
|
}
|
|
775
846
|
}
|
package/lib/register-tools.js
CHANGED
|
@@ -49,13 +49,15 @@ export function registerAllTools(ctx, deps) {
|
|
|
49
49
|
if (currentCfg && currentCfg.enabled === false) {
|
|
50
50
|
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
51
51
|
}
|
|
52
|
-
const
|
|
52
|
+
const safeArgs = args || {}
|
|
53
|
+
const safeExec = exec || {}
|
|
54
|
+
const callId = safeExec.callId || safeExec.rootCallId
|
|
53
55
|
if (callId) {
|
|
54
|
-
createLiveProgressSession(callId, { signal:
|
|
55
|
-
|
|
56
|
+
createLiveProgressSession(callId, { signal: safeExec.signal })
|
|
57
|
+
safeExec.onProgress = (update) => publishLiveProgress(callId, update)
|
|
56
58
|
}
|
|
57
59
|
try {
|
|
58
|
-
const res = await originalExecute(
|
|
60
|
+
const res = await originalExecute(safeArgs, safeExec)
|
|
59
61
|
if (callId) completeLiveProgress(callId)
|
|
60
62
|
return res
|
|
61
63
|
} catch (err) {
|
package/lib/tools/anchor.js
CHANGED
|
@@ -34,6 +34,11 @@ export function registerAnchorTools(ctx, deps) {
|
|
|
34
34
|
enum: ['style', 'character', 'face'],
|
|
35
35
|
description: 'Anchor mode: "face" preserves facial portrait identity via FaceID/IP-Adapter; "character" preserves overall character traits; "style" preserves aesthetic palette. Default: style.',
|
|
36
36
|
},
|
|
37
|
+
anchor_type: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: ['style', 'character', 'face'],
|
|
40
|
+
description: 'Alias for mode ("style", "character", "face").',
|
|
41
|
+
},
|
|
37
42
|
strength: {
|
|
38
43
|
type: 'number',
|
|
39
44
|
description: 'Reference influence strength between 0.1 and 1.0 (default 0.65).',
|
|
@@ -95,9 +100,11 @@ export function registerAnchorTools(ctx, deps) {
|
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
const imageRef = args.image.trim()
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
103
|
+
const rawMode = String(args.mode || args.anchor_type || '').toLowerCase().trim()
|
|
104
|
+
const mode = (rawMode === 'character' || rawMode === 'face') ? rawMode : 'style'
|
|
105
|
+
const strength = normalizeAnchorStrength(args.strength, mode === 'face' || mode === 'character' ? 0.75 : 0.65)
|
|
106
|
+
const defaultLabel = mode === 'face' ? 'Face Identity Anchor' : (mode === 'character' ? 'Character Anchor' : 'Style Anchor')
|
|
107
|
+
const label = args.label ? String(args.label).trim() : defaultLabel
|
|
101
108
|
|
|
102
109
|
// Validate source readability if it is a local path or attachment
|
|
103
110
|
if (resolveSource && (imageRef.startsWith('sha256:') || !imageRef.startsWith('http'))) {
|
|
@@ -107,6 +107,10 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
107
107
|
throw new Error('Image generation is disabled in settings.')
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
if (!args?.prompt || typeof args.prompt !== 'string' || !args.prompt.trim()) {
|
|
111
|
+
throw new Error('Character description prompt is required for character sheet generation.')
|
|
112
|
+
}
|
|
113
|
+
|
|
110
114
|
const provider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
|
|
111
115
|
? (args.provider || cfg.provider)
|
|
112
116
|
: (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
|
package/lib/tools/diagram.js
CHANGED
|
@@ -103,6 +103,10 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
103
103
|
throw new Error('Image generation is disabled in settings.')
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if (!args?.diagram || typeof args.diagram !== 'string' || !args.diagram.trim()) {
|
|
107
|
+
throw new Error('Diagram content (Mermaid code, ASCII, or description) is required for beautify_diagram.')
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
const provider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
|
|
107
111
|
? (args.provider || cfg.provider)
|
|
108
112
|
: (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
|
|
@@ -30,7 +30,10 @@ export function registerGenerationPackTool(ctx, deps) {
|
|
|
30
30
|
},
|
|
31
31
|
async execute(args, exec) {
|
|
32
32
|
const ratios = Array.isArray(args.aspect_ratios) && args.aspect_ratios.length ? args.aspect_ratios : ['1:1', '16:9', '9:16']
|
|
33
|
-
|
|
33
|
+
const generateTool = ctx?.tools?.get ? ctx.tools.get('generate_image') : (ctx?.tools?.list?.find ? ctx.tools.list.find((t) => t.name === 'generate_image') : null)
|
|
34
|
+
if (!generateTool || typeof generateTool.execute !== 'function') {
|
|
35
|
+
throw new Error('Required tool "generate_image" is not available on host.')
|
|
36
|
+
}
|
|
34
37
|
const results = []
|
|
35
38
|
const warnings = []
|
|
36
39
|
const seedBase = Math.floor(Math.random() * 100000)
|
package/lib/tools/generation.js
CHANGED
|
@@ -300,7 +300,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
300
300
|
const cfg = live()
|
|
301
301
|
|
|
302
302
|
// #168: Fail-fast loop guard protecting against runaway agent loops
|
|
303
|
-
const sessionId = exec
|
|
303
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
304
304
|
if (cfg.loopGuardLimit > 0) {
|
|
305
305
|
trackAndAssertLoopGuard(sessionId, {
|
|
306
306
|
limit: cfg.loopGuardLimit,
|
|
@@ -326,6 +326,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
326
326
|
effectiveGuidance,
|
|
327
327
|
activeAnchor,
|
|
328
328
|
resolvedFaceRef,
|
|
329
|
+
resolvedRefImage,
|
|
329
330
|
} = await prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId, provider, resolveSource })
|
|
330
331
|
|
|
331
332
|
// #167: Assert daily budget availability before calling API
|
|
@@ -339,7 +340,28 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
339
340
|
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
340
341
|
const providers = makeProviders(
|
|
341
342
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
342
|
-
{
|
|
343
|
+
{
|
|
344
|
+
prompt: effectivePrompt,
|
|
345
|
+
size,
|
|
346
|
+
format,
|
|
347
|
+
seed: args.seed,
|
|
348
|
+
signal: exec?.signal,
|
|
349
|
+
negativePrompt: effectiveNegative,
|
|
350
|
+
guidanceScale: effectiveGuidance,
|
|
351
|
+
source,
|
|
352
|
+
mask,
|
|
353
|
+
strength: args.strength,
|
|
354
|
+
quality: args.quality,
|
|
355
|
+
style: args.style,
|
|
356
|
+
aspectPixels,
|
|
357
|
+
aspectRatio: args.aspect_ratio,
|
|
358
|
+
faceReference: resolvedFaceRef,
|
|
359
|
+
faceStrength: args.face_strength || activeAnchor?.strength,
|
|
360
|
+
referenceImage: resolvedRefImage,
|
|
361
|
+
referenceStrength: args.reference_strength || activeAnchor?.strength,
|
|
362
|
+
onProgress: exec?.onProgress,
|
|
363
|
+
callId: exec?.callId,
|
|
364
|
+
},
|
|
343
365
|
)
|
|
344
366
|
// Subscription providers return {ok:false, reason} instead of throwing:
|
|
345
367
|
// rejection reaches the model as text. Without this guard,
|
|
@@ -445,7 +467,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
445
467
|
const stem = `${slugify(args.output_name || promptVal)}-${Date.now().toString(36)}-${seedVal}`
|
|
446
468
|
const name = `${stem}.${extension}`
|
|
447
469
|
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: diskHit.bytes, mediaType: diskHit.mediaType, name })
|
|
448
|
-
const sessionCwd = exec
|
|
470
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
449
471
|
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
450
472
|
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
451
473
|
await mkdir(outDir, { recursive: true })
|
|
@@ -58,12 +58,12 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
58
58
|
assertBudgetAvailable(0.02, cfg.dailyBudgetUsd)
|
|
59
59
|
const res = await removeBackgroundFal(
|
|
60
60
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
61
|
-
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec
|
|
61
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec?.signal }
|
|
62
62
|
)
|
|
63
63
|
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
64
64
|
const name = `${stem}.png`
|
|
65
65
|
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
66
|
-
const sessionCwd = exec
|
|
66
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
67
67
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
68
68
|
await mkdir(outDir, { recursive: true })
|
|
69
69
|
const filePath = path.join(outDir, name)
|
|
@@ -129,12 +129,12 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
129
129
|
assertBudgetAvailable(0.025, cfg.dailyBudgetUsd)
|
|
130
130
|
const res = await upscaleImageFal(
|
|
131
131
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
132
|
-
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec
|
|
132
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec?.signal }
|
|
133
133
|
)
|
|
134
134
|
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
135
135
|
const name = `${stem}.png`
|
|
136
136
|
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
137
|
-
const sessionCwd = exec
|
|
137
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
138
138
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
139
139
|
await mkdir(outDir, { recursive: true })
|
|
140
140
|
const filePath = path.join(outDir, name)
|
|
@@ -196,7 +196,7 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
196
196
|
const res = await traceToSvg(source.bytes, { colorMode: args.color_mode })
|
|
197
197
|
const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
|
|
198
198
|
const name = `${stem}.svg`
|
|
199
|
-
const sessionCwd = exec
|
|
199
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
200
200
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
201
201
|
await mkdir(outDir, { recursive: true })
|
|
202
202
|
const filePath = path.join(outDir, name)
|
|
@@ -268,12 +268,12 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
268
268
|
}
|
|
269
269
|
const res = await blendImagesFal(
|
|
270
270
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
271
|
-
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec
|
|
271
|
+
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec?.signal }
|
|
272
272
|
)
|
|
273
273
|
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
274
274
|
const name = `${stem}.png`
|
|
275
275
|
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
276
|
-
const sessionCwd = exec
|
|
276
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
277
277
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
278
278
|
await mkdir(outDir, { recursive: true })
|
|
279
279
|
const filePath = path.join(outDir, name)
|
|
@@ -151,7 +151,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
151
151
|
const quadrantIds = ['A', 'B', 'C', 'D']
|
|
152
152
|
const matrixId = `matrix-${Date.now().toString(36)}`
|
|
153
153
|
|
|
154
|
-
const sessionCwd = exec
|
|
154
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
155
155
|
const targetDir = cfg.outputDir || 'generated/images'
|
|
156
156
|
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
157
157
|
await mkdir(outDir, { recursive: true })
|
|
@@ -168,7 +168,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
168
168
|
size: 'custom',
|
|
169
169
|
format: 'png',
|
|
170
170
|
seed: cellSeed,
|
|
171
|
-
signal: exec
|
|
171
|
+
signal: exec?.signal,
|
|
172
172
|
aspectPixels,
|
|
173
173
|
aspectRatio,
|
|
174
174
|
},
|
package/lib/tools/theme-pair.js
CHANGED
|
@@ -158,7 +158,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
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)
|
|
160
160
|
|
|
161
|
-
const sessionCwd = exec
|
|
161
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
162
162
|
const targetDir = cfg.outputDir || 'generated/images'
|
|
163
163
|
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
164
164
|
await mkdir(outDir, { recursive: true })
|
|
@@ -176,7 +176,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
176
176
|
aspectPixels,
|
|
177
177
|
aspectRatio,
|
|
178
178
|
seed: baseSeed,
|
|
179
|
-
signal: exec
|
|
179
|
+
signal: exec?.signal,
|
|
180
180
|
}
|
|
181
181
|
const lightProviders = makeProviders(
|
|
182
182
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
@@ -214,7 +214,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
214
214
|
aspectPixels,
|
|
215
215
|
aspectRatio,
|
|
216
216
|
seed: baseSeed,
|
|
217
|
-
signal: exec
|
|
217
|
+
signal: exec?.signal,
|
|
218
218
|
}
|
|
219
219
|
const darkProviders = makeProviders(
|
|
220
220
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
package/lib/tools/ui-asset.js
CHANGED
|
@@ -156,7 +156,7 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
156
156
|
size: 'custom',
|
|
157
157
|
format: 'png',
|
|
158
158
|
seed: args.seed,
|
|
159
|
-
signal: exec
|
|
159
|
+
signal: exec?.signal,
|
|
160
160
|
aspectPixels,
|
|
161
161
|
aspectRatio,
|
|
162
162
|
},
|
|
@@ -187,7 +187,7 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
187
187
|
try {
|
|
188
188
|
const bgResult = await removeBackgroundFal(
|
|
189
189
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
190
|
-
{ imageBytes: finalBytes, mediaType, signal: exec
|
|
190
|
+
{ imageBytes: finalBytes, mediaType, signal: exec?.signal },
|
|
191
191
|
)
|
|
192
192
|
if (bgResult && bgResult.bytes) {
|
|
193
193
|
finalBytes = bgResult.bytes
|
|
@@ -216,7 +216,7 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
216
216
|
meta: { provider, model: cfg.model || cfg.customModel, prompt: synthesizedPrompt, seed: seedVal },
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
const sessionCwd = exec
|
|
219
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
220
220
|
const targetDir = cfg.outputDir || 'generated/images'
|
|
221
221
|
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
222
222
|
await mkdir(outDir, { recursive: true })
|
package/lib/tools/vision-ocr.js
CHANGED
|
@@ -137,6 +137,10 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
137
137
|
throw new Error('Image generation is disabled in settings.')
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
if (!args?.image || typeof args.image !== 'string' || !args.image.trim()) {
|
|
141
|
+
throw new Error('Parameter "image" (URL, file path, or sha256 attachment ID) is required for text replacement.')
|
|
142
|
+
}
|
|
143
|
+
|
|
140
144
|
const provider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
|
|
141
145
|
? (args.provider || cfg.provider)
|
|
142
146
|
: (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
|
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.29",
|
|
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",
|
|
7
7
|
"dsh",
|
|
@@ -57,6 +57,10 @@
|
|
|
57
57
|
]
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
62
|
+
"sharp": "^0.33.5 || ^0.35.0"
|
|
63
|
+
},
|
|
60
64
|
"peerDependencies": {
|
|
61
65
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
62
66
|
"@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|