@goodandready/dsh-image-gen 0.11.26 → 0.11.28
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/attachment-helper.js +25 -0
- package/lib/client.js +179 -39
- package/lib/comfy-workflow-helpers.js +14 -0
- package/lib/generation-helpers.js +20 -3
- package/lib/index.js +4 -0
- package/lib/live-progress.js +278 -0
- package/lib/providers/backends/custom.js +14 -2
- package/lib/providers/backends/fal.js +25 -3
- package/lib/providers/backends/gemini.js +12 -0
- package/lib/providers/backends/local.js +148 -48
- 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/shared-helpers.js +63 -1
- package/lib/providers.js +142 -71
- package/lib/register-tools.js +21 -1
- 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/editing.js +17 -0
- package/lib/tools/generation-pack.js +4 -1
- package/lib/tools/generation.js +31 -4
- package/lib/tools/processing-basic.js +7 -7
- package/lib/tools/style-matrix.js +11 -3
- package/lib/tools/theme-pair.js +22 -4
- package/lib/tools/ui-asset.js +3 -3
- package/lib/tools/vision-ocr.js +4 -0
- package/package.json +2 -2
|
@@ -115,7 +115,7 @@ export async function submitJob(fetchImpl, baseURL, model, key, body, signal) {
|
|
|
115
115
|
return data
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
export async function pollStatus(fetchImpl, statusUrl, key, signal, pollIntervalMs = 500, timeoutMs = 180000) {
|
|
118
|
+
export async function pollStatus(fetchImpl, statusUrl, key, signal, pollIntervalMs = 500, timeoutMs = 180000, onProgress) {
|
|
119
119
|
const deadline = Date.now() + timeoutMs
|
|
120
120
|
let attempt = 0
|
|
121
121
|
for (;;) {
|
|
@@ -138,6 +138,21 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
|
|
|
138
138
|
const res = await fetchImpl(statusUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
|
|
139
139
|
const data = await res.json().catch(() => ({}))
|
|
140
140
|
const status = data.status
|
|
141
|
+
if (typeof onProgress === 'function') {
|
|
142
|
+
if (status === 'IN_QUEUE') {
|
|
143
|
+
const pos = data.queue_position
|
|
144
|
+
onProgress({
|
|
145
|
+
stage: pos !== undefined ? `In queue (#${pos})` : 'In queue',
|
|
146
|
+
progress: undefined, // honest status: do NOT simulate fake percentage
|
|
147
|
+
})
|
|
148
|
+
} else if (status === 'IN_PROGRESS') {
|
|
149
|
+
const rawProg = typeof data.progress === 'number' ? Math.round(data.progress * 100) : undefined
|
|
150
|
+
onProgress({
|
|
151
|
+
stage: 'Generating...',
|
|
152
|
+
progress: rawProg,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
141
156
|
if (status === 'COMPLETED') return data
|
|
142
157
|
if (status === 'ERROR' || data.error || data.detail) {
|
|
143
158
|
throw new Error(`FAL generation failed: ${JSON.stringify(data).slice(0, 600)}`)
|
|
@@ -148,6 +163,53 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
|
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Normalizes mask bytes for OpenAI/Custom inpainting where transparent alpha (0) indicates edit region.
|
|
168
|
+
* If the incoming mask has an alpha channel with transparent areas, it is preserved.
|
|
169
|
+
* If the incoming mask is opaque black & white (e.g. from Canvas), white pixels are converted to transparent (alpha 0)
|
|
170
|
+
* and black pixels to opaque (alpha 255).
|
|
171
|
+
*
|
|
172
|
+
* @param {Buffer|Uint8Array} maskBytes
|
|
173
|
+
* @returns {Promise<Buffer>}
|
|
174
|
+
*/
|
|
175
|
+
export async function normalizeMaskAlpha(maskBytes) {
|
|
176
|
+
if (!maskBytes || maskBytes.length === 0) return maskBytes
|
|
177
|
+
try {
|
|
178
|
+
const img = sharp(maskBytes)
|
|
179
|
+
const { data, info } = await img.raw().ensureAlpha().toBuffer({ resolveWithObject: true })
|
|
180
|
+
const { width, height, channels } = info
|
|
181
|
+
|
|
182
|
+
let hasTransparency = false
|
|
183
|
+
for (let i = 3; i < data.length; i += channels) {
|
|
184
|
+
if (data[i] < 250) {
|
|
185
|
+
hasTransparency = true
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (hasTransparency) {
|
|
191
|
+
return await sharp(data, { raw: { width, height, channels } }).png().toBuffer()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const out = Buffer.from(data)
|
|
195
|
+
for (let i = 0; i < out.length; i += channels) {
|
|
196
|
+
const r = out[i]
|
|
197
|
+
const g = out[i + 1]
|
|
198
|
+
const b = out[i + 2]
|
|
199
|
+
const lum = 0.299 * r + 0.587 * g + 0.114 * b
|
|
200
|
+
if (lum > 128) {
|
|
201
|
+
out[i + 3] = 0
|
|
202
|
+
} else {
|
|
203
|
+
out[i + 3] = 255
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return await sharp(out, { raw: { width, height, channels } }).png().toBuffer()
|
|
208
|
+
} catch (_) {
|
|
209
|
+
return maskBytes
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
151
213
|
export function buildEditForm({ source, mask, prompt, size, strength, model }) {
|
|
152
214
|
const form = new FormData()
|
|
153
215
|
if (model) form.append('model', model)
|
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
|
@@ -19,6 +19,12 @@ import { registerThemePairTools } from './tools/theme-pair.js'
|
|
|
19
19
|
import { registerCharacterSheetTools } from './tools/character-sheet.js'
|
|
20
20
|
import { registerVisionOcrTools } from './tools/vision-ocr.js'
|
|
21
21
|
import { registerDiagramTools } from './tools/diagram.js'
|
|
22
|
+
import {
|
|
23
|
+
createLiveProgressSession,
|
|
24
|
+
publishLiveProgress,
|
|
25
|
+
completeLiveProgress,
|
|
26
|
+
abortLiveProgress,
|
|
27
|
+
} from './live-progress.js'
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Registers every image-gen tool on the host tools service according to toolsetProfile (#337, #384).
|
|
@@ -43,7 +49,21 @@ export function registerAllTools(ctx, deps) {
|
|
|
43
49
|
if (currentCfg && currentCfg.enabled === false) {
|
|
44
50
|
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
45
51
|
}
|
|
46
|
-
|
|
52
|
+
const safeArgs = args || {}
|
|
53
|
+
const safeExec = exec || {}
|
|
54
|
+
const callId = safeExec.callId || safeExec.rootCallId
|
|
55
|
+
if (callId) {
|
|
56
|
+
createLiveProgressSession(callId, { signal: safeExec.signal })
|
|
57
|
+
safeExec.onProgress = (update) => publishLiveProgress(callId, update)
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const res = await originalExecute(safeArgs, safeExec)
|
|
61
|
+
if (callId) completeLiveProgress(callId)
|
|
62
|
+
return res
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (callId) abortLiveProgress(callId, err.message)
|
|
65
|
+
throw err
|
|
66
|
+
}
|
|
47
67
|
},
|
|
48
68
|
}
|
|
49
69
|
const res = originalToolsRegister(wrappedDef)
|
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')
|
package/lib/tools/editing.js
CHANGED
|
@@ -98,6 +98,21 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
98
98
|
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
99
99
|
const maskRef = args.mask_image || args.mask
|
|
100
100
|
const mask = maskRef ? await resolveSource(ctx, exec, maskRef) : undefined
|
|
101
|
+
if (mask && mask.bytes && source && source.bytes) {
|
|
102
|
+
try {
|
|
103
|
+
const sharpMod = await import('sharp')
|
|
104
|
+
const sharp = sharpMod.default || sharpMod
|
|
105
|
+
const srcMeta = await sharp(source.bytes).metadata()
|
|
106
|
+
const maskMeta = await sharp(mask.bytes).metadata()
|
|
107
|
+
if (srcMeta.width && srcMeta.height && (maskMeta.width !== srcMeta.width || maskMeta.height !== srcMeta.height)) {
|
|
108
|
+
const resized = await sharp(mask.bytes).resize(srcMeta.width, srcMeta.height, { fit: 'fill' }).png().toBuffer()
|
|
109
|
+
mask.bytes = resized
|
|
110
|
+
mask.mediaType = 'image/png'
|
|
111
|
+
}
|
|
112
|
+
} catch (_err) {
|
|
113
|
+
// Mask dimension normalization failure ignored
|
|
114
|
+
}
|
|
115
|
+
}
|
|
101
116
|
const vision = await analyzeImageWithVision(ctx, exec, source)
|
|
102
117
|
|
|
103
118
|
let effectivePrompt = args.prompt
|
|
@@ -127,6 +142,8 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
127
142
|
strength,
|
|
128
143
|
provider,
|
|
129
144
|
signal: exec?.signal,
|
|
145
|
+
onProgress: exec?.onProgress,
|
|
146
|
+
callId: exec?.callId,
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
const gen = await editImageDirect(deps, job)
|
|
@@ -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPresentationMeta } from '../attachment-helper.js'
|
|
1
2
|
import { registerGenerationPackTool } from './generation-pack.js'
|
|
2
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -282,12 +283,16 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
282
283
|
const summary = lines.join('\n')
|
|
283
284
|
if (live().deliverAs !== 'image') {
|
|
284
285
|
const urls = list.map((img) => img.url).filter(Boolean)
|
|
285
|
-
|
|
286
|
+
const mdLinks = list.map((img) => ``).join('\n')
|
|
287
|
+
return [{ type: 'text', text: urls.length ? `${summary}\n${mdLinks}` : summary }]
|
|
286
288
|
}
|
|
287
289
|
const blocks = [{ type: 'text', text: summary }]
|
|
288
290
|
for (const img of list) if (img.attachment) blocks.push({ type: 'image', attachment: img.attachment })
|
|
289
291
|
return blocks
|
|
290
292
|
},
|
|
293
|
+
presentationMeta(args, value) {
|
|
294
|
+
return buildPresentationMeta(value)
|
|
295
|
+
},
|
|
291
296
|
},
|
|
292
297
|
isConcurrencySafe: () => false,
|
|
293
298
|
timeoutMs: (config?.timeoutMs || 180000) + 30000,
|
|
@@ -295,7 +300,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
295
300
|
const cfg = live()
|
|
296
301
|
|
|
297
302
|
// #168: Fail-fast loop guard protecting against runaway agent loops
|
|
298
|
-
const sessionId = exec
|
|
303
|
+
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
299
304
|
if (cfg.loopGuardLimit > 0) {
|
|
300
305
|
trackAndAssertLoopGuard(sessionId, {
|
|
301
306
|
limit: cfg.loopGuardLimit,
|
|
@@ -321,6 +326,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
321
326
|
effectiveGuidance,
|
|
322
327
|
activeAnchor,
|
|
323
328
|
resolvedFaceRef,
|
|
329
|
+
resolvedRefImage,
|
|
324
330
|
} = await prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId, provider, resolveSource })
|
|
325
331
|
|
|
326
332
|
// #167: Assert daily budget availability before calling API
|
|
@@ -334,7 +340,28 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
334
340
|
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
335
341
|
const providers = makeProviders(
|
|
336
342
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
337
|
-
{
|
|
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
|
+
},
|
|
338
365
|
)
|
|
339
366
|
// Subscription providers return {ok:false, reason} instead of throwing:
|
|
340
367
|
// rejection reaches the model as text. Without this guard,
|
|
@@ -440,7 +467,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
440
467
|
const stem = `${slugify(args.output_name || promptVal)}-${Date.now().toString(36)}-${seedVal}`
|
|
441
468
|
const name = `${stem}.${extension}`
|
|
442
469
|
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: diskHit.bytes, mediaType: diskHit.mediaType, name })
|
|
443
|
-
const sessionCwd = exec
|
|
470
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
444
471
|
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
445
472
|
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
446
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)
|