@goodandready/dsh-image-gen 0.10.0 → 0.10.2
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 +29 -23
- package/lib/index.js +70 -16
- package/lib/providers.js +44 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -461,10 +461,12 @@ window.__ModuleLoader__.load({
|
|
|
461
461
|
}
|
|
462
462
|
const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
463
463
|
const DELIVERY_MODES = ['link', 'image']
|
|
464
|
-
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local']
|
|
464
|
+
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
465
465
|
|
|
466
466
|
/** Field specs in display order. */
|
|
467
|
+
const booleanField = (name) => ({ name, type: 'boolean', parse: (v) => v === 'true' || v === true, format: (v) => String(!!v) })
|
|
467
468
|
const FIELDS = [
|
|
469
|
+
{ field: 'enabled', spec: booleanField('enabled'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enabled', hintKey: 'f.enabledHint' },
|
|
468
470
|
{ field: 'provider', spec: selectField('provider', PROVIDERS), kind: 'select', options: PROVIDERS, labelKey: 'f.provider', hintKey: 'f.providerHint', providerNeeds: { fal: 'ключ FAL', custom: 'адрес + ключ', codex: 'подписка ChatGPT', grok: 'подписка Grok', local: 'адрес сервера' } },
|
|
469
471
|
{ field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'] },
|
|
470
472
|
{ field: 'apiKeyEnv', spec: textField('apiKeyEnv'), kind: 'text', labelKey: 'f.apiKeyEnv', hintKey: 'f.apiKeyEnvHint', placeholderKey: 'p.apiKeyEnv', when: ['fal'] },
|
|
@@ -808,17 +810,38 @@ window.__ModuleLoader__.load({
|
|
|
808
810
|
}
|
|
809
811
|
}
|
|
810
812
|
|
|
811
|
-
|
|
813
|
+
function FalImageCard(props) {
|
|
812
814
|
const block = props.block
|
|
813
815
|
const running = !('kind' in (block || {}))
|
|
814
816
|
const failed = block && (block.isError || block.error !== undefined)
|
|
815
817
|
const parsed = readResult(block)
|
|
816
|
-
const [editing, setEditing] = react.useState(false)
|
|
817
818
|
|
|
818
|
-
|
|
819
|
+
let args = {}
|
|
820
|
+
try {
|
|
821
|
+
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
822
|
+
if (raw) args = JSON.parse(raw)
|
|
823
|
+
} catch (e) { /* ignore */ }
|
|
819
824
|
|
|
825
|
+
let prompt = ''
|
|
826
|
+
try {
|
|
827
|
+
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
828
|
+
prompt = raw ? (JSON.parse(raw).prompt || '') : ''
|
|
829
|
+
} catch (e) { prompt = '' }
|
|
830
|
+
|
|
831
|
+
const repeatText = (() => {
|
|
832
|
+
const parts = [String(args.prompt || '').trim()]
|
|
833
|
+
const size = args.image_size || args.size
|
|
834
|
+
const seed = args.seed
|
|
835
|
+
if (size) parts.push(`Размер: ${size}`)
|
|
836
|
+
if (seed !== undefined) parts.push(`Сид: ${seed}`)
|
|
837
|
+
return parts.filter(Boolean).join(', ')
|
|
838
|
+
})()
|
|
839
|
+
|
|
840
|
+
const [editing, setEditing] = react.useState(false)
|
|
841
|
+
const [inpaintOpen, setInpaintOpen] = react.useState(false)
|
|
820
842
|
const [compareSlider, setCompareSlider] = react.useState(50)
|
|
821
843
|
const [showCompare, setShowCompare] = react.useState(false)
|
|
844
|
+
const [editText, setEditText] = react.useState('')
|
|
822
845
|
|
|
823
846
|
const onEditPrompt = () => {
|
|
824
847
|
const textToInsert = prompt || repeatText || ''
|
|
@@ -883,12 +906,7 @@ window.__ModuleLoader__.load({
|
|
|
883
906
|
sendActionPrompt(`Удали фон у этого изображения: remove_background(image: "${ref}")`)
|
|
884
907
|
}
|
|
885
908
|
|
|
886
|
-
|
|
887
|
-
let prompt = ''
|
|
888
|
-
try {
|
|
889
|
-
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
890
|
-
prompt = raw ? (JSON.parse(raw).prompt || '') : ''
|
|
891
|
-
} catch (e) { prompt = '' }
|
|
909
|
+
|
|
892
910
|
|
|
893
911
|
const head = react.createElement('div', { className: 'fal-head' },
|
|
894
912
|
running ? 'Генерирую изображение…' : failed ? 'Генерация не удалась' : 'Изображение')
|
|
@@ -897,19 +915,7 @@ window.__ModuleLoader__.load({
|
|
|
897
915
|
// промптом и параметрами, извлечёнными из исходного вызова инструмента.
|
|
898
916
|
// Дальше модель сама решает, как сгенерировать, — это просто повторный
|
|
899
917
|
// запрос, а не вызов инструмента из карточки.
|
|
900
|
-
|
|
901
|
-
try {
|
|
902
|
-
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
903
|
-
if (raw) args = JSON.parse(raw)
|
|
904
|
-
} catch (e) { /* ignore */ }
|
|
905
|
-
const repeatText = (() => {
|
|
906
|
-
const parts = [String(args.prompt || '').trim()]
|
|
907
|
-
const size = args.image_size || args.size
|
|
908
|
-
const seed = args.seed
|
|
909
|
-
if (size) parts.push(`Размер: ${size}`)
|
|
910
|
-
if (seed !== undefined) parts.push(`Сид: ${seed}`)
|
|
911
|
-
return parts.filter(Boolean).join(', ')
|
|
912
|
-
})()
|
|
918
|
+
|
|
913
919
|
const repeat = async () => {
|
|
914
920
|
try {
|
|
915
921
|
const sessions = props.sessions
|
package/lib/index.js
CHANGED
|
@@ -16,10 +16,14 @@ import z from '@deepseek-ai/schemastery'
|
|
|
16
16
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
17
17
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
18
18
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
19
|
+
import { existsSync, unlinkSync, accessSync } from 'node:fs'
|
|
19
20
|
import os from 'node:os'
|
|
20
21
|
import path from 'node:path'
|
|
21
22
|
import {
|
|
22
23
|
computeGenerationHash,
|
|
24
|
+
clampProviderCount,
|
|
25
|
+
buildEndpointUrl,
|
|
26
|
+
createAbortError,
|
|
23
27
|
IMAGE_SIZES,
|
|
24
28
|
OUTPUT_FORMATS,
|
|
25
29
|
PROVIDER_KEYS,
|
|
@@ -54,9 +58,13 @@ const NS = 'dsh-image-gen'
|
|
|
54
58
|
// настройки лежат под старым именем. Оно читается как запасное и переносится
|
|
55
59
|
// под новое имя один раз — молча, при первом запуске после обновления.
|
|
56
60
|
const LEGACY_NS = 'dsh-fal-image-gen'
|
|
57
|
-
export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm']
|
|
61
|
+
export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm', 'systemPrompt']
|
|
58
62
|
|
|
59
63
|
export const Config = z.object({
|
|
64
|
+
enabled: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.description('Master switch for image generation. When false, tools refuse to execute.')
|
|
67
|
+
.default(true),
|
|
60
68
|
provider: z
|
|
61
69
|
.string()
|
|
62
70
|
.description(`Which provider generates the image. One of: ${PROVIDER_KEYS.join(', ')}. `
|
|
@@ -68,6 +76,7 @@ export const Config = z.object({
|
|
|
68
76
|
.default('fal-ai/flux-2/klein/9b'),
|
|
69
77
|
apiKeyEnv: z
|
|
70
78
|
.string()
|
|
79
|
+
.role('credential-ref')
|
|
71
80
|
.description('Credential reference / env var holding the FAL API key (the "Key " auth prefix is added automatically when missing).')
|
|
72
81
|
.default('FAL_API_KEY'),
|
|
73
82
|
baseURL: z
|
|
@@ -109,6 +118,7 @@ export const Config = z.object({
|
|
|
109
118
|
.default(''),
|
|
110
119
|
customKeyEnv: z
|
|
111
120
|
.string()
|
|
121
|
+
.role('credential-ref')
|
|
112
122
|
.description('provider=custom: credential reference / env var holding the API key. '
|
|
113
123
|
+ 'Empty means no authorization header, for gateways that need none.')
|
|
114
124
|
.default('OPENAI_API_KEY'),
|
|
@@ -119,6 +129,7 @@ export const Config = z.object({
|
|
|
119
129
|
.default('black-forest-labs/flux-schnell'),
|
|
120
130
|
replicateKeyEnv: z
|
|
121
131
|
.string()
|
|
132
|
+
.role('credential-ref')
|
|
122
133
|
.description('provider=replicate: credential reference / env var holding API token.')
|
|
123
134
|
.default('REPLICATE_API_TOKEN'),
|
|
124
135
|
|
|
@@ -154,6 +165,7 @@ export const Config = z.object({
|
|
|
154
165
|
.default(7),
|
|
155
166
|
seedreamKeyEnv: z
|
|
156
167
|
.string()
|
|
168
|
+
.role('credential-ref')
|
|
157
169
|
.description('provider=seedream: credential reference / env var holding the API key.')
|
|
158
170
|
.default('SEEDREAM_API_KEY'),
|
|
159
171
|
seedreamModel: z
|
|
@@ -162,6 +174,7 @@ export const Config = z.object({
|
|
|
162
174
|
.default('seedream-4.0'),
|
|
163
175
|
geminiKeyEnv: z
|
|
164
176
|
.string()
|
|
177
|
+
.role('credential-ref')
|
|
165
178
|
.description('provider=gemini: credential reference / env var holding the Google API key.')
|
|
166
179
|
.default('GEMINI_API_KEY'),
|
|
167
180
|
geminiModel: z
|
|
@@ -217,7 +230,12 @@ export async function resolveSource(ctx, exec, ref) {
|
|
|
217
230
|
}
|
|
218
231
|
const p = path.resolve(sessionCwd || process.cwd(), ref)
|
|
219
232
|
const bytes = await readFile(p)
|
|
220
|
-
|
|
233
|
+
const ext = path.extname(p).toLowerCase()
|
|
234
|
+
const mediaType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
|
|
235
|
+
: ext === '.webp' ? 'image/webp'
|
|
236
|
+
: ext === '.svg' ? 'image/svg+xml'
|
|
237
|
+
: 'image/png'
|
|
238
|
+
return { bytes, mediaType }
|
|
221
239
|
}
|
|
222
240
|
|
|
223
241
|
export function slugify(input) {
|
|
@@ -295,7 +313,7 @@ export async function findCached(entries, seed, prompt) {
|
|
|
295
313
|
/** Вернуть запись из кэша, если файл существует; иначе undefined. */
|
|
296
314
|
export async function cachedResult(entry) {
|
|
297
315
|
if (!entry || !entry.path) return undefined
|
|
298
|
-
|
|
316
|
+
if (!entry.path || !existsSync(entry.path)) return undefined
|
|
299
317
|
return {
|
|
300
318
|
path: entry.path,
|
|
301
319
|
url: entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : '',
|
|
@@ -326,8 +344,8 @@ export async function pruneHistory(entries, pruneDays) {
|
|
|
326
344
|
for (const e of entries) {
|
|
327
345
|
const created = e.createdAt ? Date.parse(e.createdAt) : NaN
|
|
328
346
|
if (Number.isFinite(created) && created < cutoff) {
|
|
329
|
-
try {
|
|
330
|
-
try {
|
|
347
|
+
try { unlinkSync(e.path) } catch (err) { /* файл уже удалён */ }
|
|
348
|
+
try { unlinkSync(e.path.replace(/\.[^.]+$/, '.json')) } catch (err) { /* sidecar */ }
|
|
331
349
|
continue
|
|
332
350
|
}
|
|
333
351
|
kept.push(e)
|
|
@@ -408,6 +426,17 @@ export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
|
|
|
408
426
|
}
|
|
409
427
|
|
|
410
428
|
export function apply(ctx, config) {
|
|
429
|
+
if (ctx.systemPrompt && typeof ctx.systemPrompt.section === 'function') {
|
|
430
|
+
ctx.systemPrompt.section({
|
|
431
|
+
name: 'tool:image-generation',
|
|
432
|
+
order: 121,
|
|
433
|
+
text: 'To create images, call generate_image with a prompt and optional count, aspect_ratio, quality, or output_dir. '
|
|
434
|
+
+ 'The tool writes images to disk and shows them in the conversation. The model cannot see the image directly in the text stream; '
|
|
435
|
+
+ 'for quality inspection, verification of expected elements, or autonomous correction (Vision Loop), call analyze_image from the @goodandready/dsh-vision-bridge plugin '
|
|
436
|
+
+ 'or inspect_image_quality on the returned path.',
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
411
440
|
|
|
412
441
|
// The Web card edits this namespace; without registering it the card binds to
|
|
413
442
|
// a namespace nobody declared, stays unready and renders nothing — which is
|
|
@@ -494,7 +523,7 @@ export function apply(ctx, config) {
|
|
|
494
523
|
res.end(JSON.stringify({ error: 'GET only' }))
|
|
495
524
|
return
|
|
496
525
|
}
|
|
497
|
-
const exists = (p) =>
|
|
526
|
+
const exists = (p) => existsSync(p)
|
|
498
527
|
const entries = await readHistory()
|
|
499
528
|
const withThumbs = filterHistory(entries, exists).map((e) => ({
|
|
500
529
|
...e,
|
|
@@ -526,7 +555,7 @@ export function apply(ctx, config) {
|
|
|
526
555
|
},
|
|
527
556
|
aspect_ratio: {
|
|
528
557
|
type: 'string',
|
|
529
|
-
enum: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
558
|
+
enum: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
|
|
530
559
|
description: 'Optional aspect ratio; overrides image_size when both are given. Sent as width/height to providers that accept pixels.',
|
|
531
560
|
},
|
|
532
561
|
seed: {
|
|
@@ -542,6 +571,10 @@ export function apply(ctx, config) {
|
|
|
542
571
|
type: 'string',
|
|
543
572
|
description: 'Optional file name stem for the saved image (defaults to a slug of the prompt).',
|
|
544
573
|
},
|
|
574
|
+
output_dir: {
|
|
575
|
+
type: 'string',
|
|
576
|
+
description: 'Optional custom directory to write images into (relative to session cwd or absolute). Defaults to outputDir from settings.',
|
|
577
|
+
},
|
|
545
578
|
count: {
|
|
546
579
|
type: 'integer',
|
|
547
580
|
description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
|
|
@@ -566,7 +599,7 @@ export function apply(ctx, config) {
|
|
|
566
599
|
},
|
|
567
600
|
quality: {
|
|
568
601
|
type: 'string',
|
|
569
|
-
enum: ['low', 'medium', 'high'],
|
|
602
|
+
enum: ['auto', 'low', 'medium', 'high'],
|
|
570
603
|
description: 'Optional quality for providers that support it (seedream, gemini). Ignored otherwise.',
|
|
571
604
|
},
|
|
572
605
|
style: {
|
|
@@ -589,7 +622,7 @@ export function apply(ctx, config) {
|
|
|
589
622
|
output: {
|
|
590
623
|
schema: {
|
|
591
624
|
type: 'object',
|
|
592
|
-
additionalProperties:
|
|
625
|
+
additionalProperties: true,
|
|
593
626
|
properties: {
|
|
594
627
|
path: { type: 'string' },
|
|
595
628
|
url: { type: 'string' },
|
|
@@ -630,7 +663,7 @@ export function apply(ctx, config) {
|
|
|
630
663
|
},
|
|
631
664
|
render(args, value) {
|
|
632
665
|
const list = value.images || [value]
|
|
633
|
-
const lines = list.map((img) => `
|
|
666
|
+
const lines = list.map((img) => `Generated image (${img.width}×${img.height}, ${img.format}, seed ${img.seed}) via ${value.provider || 'image-gen'}/${value.model || 'default'}: ${img.path}`)
|
|
634
667
|
const summary = lines.join('\n')
|
|
635
668
|
if (live().deliverAs !== 'image') {
|
|
636
669
|
const urls = list.map((img) => img.url).filter(Boolean)
|
|
@@ -645,6 +678,9 @@ export function apply(ctx, config) {
|
|
|
645
678
|
timeoutMs: config.timeoutMs + 30000,
|
|
646
679
|
async execute(args, exec) {
|
|
647
680
|
const cfg = live()
|
|
681
|
+
if (cfg.enabled === false) {
|
|
682
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
683
|
+
}
|
|
648
684
|
const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
|
|
649
685
|
const deliverAs = cfg.deliverAs
|
|
650
686
|
const size = args.aspect_ratio ? 'custom' : (args.image_size ?? cfg.defaultSize)
|
|
@@ -664,12 +700,14 @@ export function apply(ctx, config) {
|
|
|
664
700
|
const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
|
|
665
701
|
const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
|
|
666
702
|
const enhanced = await enhancePrompt(ctx, cfg, args.prompt, exec.signal, provider)
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
const effectivePrompt =
|
|
703
|
+
const effectiveStyle = args.style || cfg.stylePreset
|
|
704
|
+
const styleInfo = resolveStylePreset(effectiveStyle, args.negative_prompt, args.guidance_scale)
|
|
705
|
+
const effectivePrompt = styleInfo.promptSuffix ? `${enhanced.prompt}, ${styleInfo.promptSuffix}` : enhanced.prompt
|
|
706
|
+
const effectiveNegative = styleInfo.negativePrompt
|
|
707
|
+
const effectiveGuidance = styleInfo.guidanceScale
|
|
670
708
|
const providers = makeProviders(
|
|
671
709
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
672
|
-
{ prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt:
|
|
710
|
+
{ prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: effectiveNegative, guidanceScale: effectiveGuidance, source, mask, strength: args.strength, quality: args.quality, style: args.style, aspectPixels },
|
|
673
711
|
)
|
|
674
712
|
// Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
|
|
675
713
|
// отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
|
|
@@ -703,7 +741,8 @@ export function apply(ctx, config) {
|
|
|
703
741
|
})
|
|
704
742
|
|
|
705
743
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
706
|
-
const
|
|
744
|
+
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
745
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
707
746
|
await mkdir(outDir, { recursive: true })
|
|
708
747
|
const filePath = path.join(outDir, name)
|
|
709
748
|
await writeFile(filePath, bytes)
|
|
@@ -734,13 +773,28 @@ export function apply(ctx, config) {
|
|
|
734
773
|
}), null, 2),
|
|
735
774
|
)
|
|
736
775
|
|
|
776
|
+
const cacheHash = computeGenerationHash({
|
|
777
|
+
provider,
|
|
778
|
+
model: cfg.model,
|
|
779
|
+
prompt: promptArg,
|
|
780
|
+
seed: gen.seed,
|
|
781
|
+
size: args.image_size || cfg.defaultSize,
|
|
782
|
+
style: args.style || cfg.stylePreset,
|
|
783
|
+
})
|
|
737
784
|
const entry = {
|
|
738
785
|
path: filePath,
|
|
739
786
|
prompt: promptArg,
|
|
740
787
|
provider,
|
|
788
|
+
model: cfg.model,
|
|
741
789
|
size,
|
|
742
790
|
format,
|
|
743
791
|
seed: gen.seed,
|
|
792
|
+
cacheHash,
|
|
793
|
+
thumbnailUrl: localUrl,
|
|
794
|
+
width: gen.width || attachment.width,
|
|
795
|
+
height: gen.height || attachment.height,
|
|
796
|
+
mediaType,
|
|
797
|
+
bytes: attachment.bytes,
|
|
744
798
|
createdAt: new Date().toISOString(),
|
|
745
799
|
attachmentId: attachment.attachmentId,
|
|
746
800
|
}
|
|
@@ -817,7 +871,7 @@ export function apply(ctx, config) {
|
|
|
817
871
|
}
|
|
818
872
|
}
|
|
819
873
|
const first = images[0]
|
|
820
|
-
return { ...first, images }
|
|
874
|
+
return { channel: provider, provider, model: cfg.model || cfg.customModel, ...first, images }
|
|
821
875
|
},
|
|
822
876
|
}),
|
|
823
877
|
)
|
package/lib/providers.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
|
|
2
|
+
export const PROVIDER_MAX_COUNTS = {
|
|
3
|
+
fal: 4,
|
|
4
|
+
replicate: 4,
|
|
5
|
+
custom: 10,
|
|
6
|
+
seedream: 10,
|
|
7
|
+
gemini: 4,
|
|
8
|
+
codex: 1,
|
|
9
|
+
grok: 1,
|
|
10
|
+
local: 4,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function clampProviderCount(provider, count) {
|
|
14
|
+
const max = PROVIDER_MAX_COUNTS[provider] || 4
|
|
15
|
+
return normalizeCount(count, max)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildEndpointUrl(baseURL, pathSuffix) {
|
|
19
|
+
const base = String(baseURL || '').trim().replace(/\/+$/, '')
|
|
20
|
+
const suffix = String(pathSuffix || '').trim().replace(/^\/+/, '')
|
|
21
|
+
return `${base}/${suffix}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createAbortError(message = 'Image generation cancelled') {
|
|
25
|
+
const err = new Error(message)
|
|
26
|
+
err.name = 'AbortError'
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
2
31
|
/** Fast Laplacian-like sharpness and entropy estimator across image scanlines. */
|
|
3
32
|
export function estimateSharpnessAndVariance(bytes) {
|
|
4
33
|
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || [])
|
|
@@ -132,10 +161,10 @@ export async function tryGenerate(generators, order, seed, promptArg) {
|
|
|
132
161
|
throw new Error(`Image generation failed on all providers. ${refusals.join('; ')}`)
|
|
133
162
|
}
|
|
134
163
|
|
|
135
|
-
export function normalizeCount(count) {
|
|
164
|
+
export function normalizeCount(count, max = 4) {
|
|
136
165
|
const n = Math.floor(Number(count))
|
|
137
166
|
if (!Number.isFinite(n)) return 1
|
|
138
|
-
return Math.max(1, Math.min(
|
|
167
|
+
return Math.max(1, Math.min(max, n))
|
|
139
168
|
}
|
|
140
169
|
|
|
141
170
|
// Размеры, которыми оперируют подписки, — свой набор, не похожий на именованные
|
|
@@ -238,7 +267,7 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
|
|
|
238
267
|
}, { once: true })
|
|
239
268
|
}
|
|
240
269
|
})
|
|
241
|
-
if (signal
|
|
270
|
+
if (signal?.aborted) throw new Error('FAL generation cancelled')
|
|
242
271
|
const res = await fetchImpl(statusUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
|
|
243
272
|
const data = await res.json().catch(() => ({}))
|
|
244
273
|
const status = data.status
|
|
@@ -268,6 +297,8 @@ export const ASPECT_RATIOS = {
|
|
|
268
297
|
'9:16': [768, 1344],
|
|
269
298
|
'4:3': [1152, 896],
|
|
270
299
|
'3:4': [896, 1152],
|
|
300
|
+
'3:2': [1152, 768],
|
|
301
|
+
'2:3': [768, 1152],
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
/** Размер в пикселях: aspectPixels (если задан) или из именованного размера. */
|
|
@@ -370,7 +401,7 @@ export function makeProviders(deps, job) {
|
|
|
370
401
|
|
|
371
402
|
// response_format не отправляем: новые модели OpenAI его отвергают, а ответ
|
|
372
403
|
// всё равно приходит либо base64, либо ссылкой — принимаем оба.
|
|
373
|
-
const endpoint = source ?
|
|
404
|
+
const endpoint = buildEndpointUrl(base, source ? 'images/edits' : 'images/generations')
|
|
374
405
|
const res = await fetchImpl(endpoint, {
|
|
375
406
|
method: 'POST',
|
|
376
407
|
headers,
|
|
@@ -383,11 +414,18 @@ export function makeProviders(deps, job) {
|
|
|
383
414
|
size: cfg.customSize || (aspectPixels ? aspectPixels.join('x') : SIZE_PIXELS[size]) || size,
|
|
384
415
|
...(negativePrompt !== undefined ? { negative_prompt: negativePrompt } : {}),
|
|
385
416
|
...(guidanceScale !== undefined ? { guidance_scale: guidanceScale } : {}),
|
|
417
|
+
...(quality && quality !== 'auto' ? { quality } : {}),
|
|
386
418
|
}),
|
|
387
419
|
signal,
|
|
388
420
|
})
|
|
389
421
|
const data = await res.json().catch(() => ({}))
|
|
390
422
|
if (!res.ok) {
|
|
423
|
+
if (res.status === 503) {
|
|
424
|
+
const msg = (data?.error?.message || JSON.stringify(data)).toLowerCase()
|
|
425
|
+
if (msg.includes('no available channel') || msg.includes('model_not_found')) {
|
|
426
|
+
throw new Error(`Custom image API has no provisioned channel for "${cfg.customModel}" (HTTP 503); check gateway channel and routing configuration`)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
391
429
|
const detail = data?.error?.message || JSON.stringify(data).slice(0, 600)
|
|
392
430
|
throw new Error(`Image API failed (HTTP ${res.status}): ${detail}`)
|
|
393
431
|
}
|
|
@@ -540,7 +578,7 @@ export function makeProviders(deps, job) {
|
|
|
540
578
|
const deadline = Date.now() + cfg.timeoutMs
|
|
541
579
|
let attempt = 0
|
|
542
580
|
for (;;) {
|
|
543
|
-
if (signal
|
|
581
|
+
if (signal?.aborted) throw new Error('Local ComfyUI generation cancelled')
|
|
544
582
|
if (Date.now() > deadline) throw new Error(`Local ComfyUI timed out after ${cfg.timeoutMs} ms`)
|
|
545
583
|
const cDelay = calculateBackoff(attempt++, cfg.pollIntervalMs || 1000, 5000)
|
|
546
584
|
await new Promise((resolve) => {
|
|
@@ -663,7 +701,7 @@ export function makeProviders(deps, job) {
|
|
|
663
701
|
const deadline = Date.now() + (cfg.timeoutMs || 180000)
|
|
664
702
|
let attempt = 0
|
|
665
703
|
while (pred.status !== 'succeeded') {
|
|
666
|
-
if (signal
|
|
704
|
+
if (signal?.aborted) throw new Error('Replicate generation cancelled')
|
|
667
705
|
if (Date.now() > deadline) throw new Error('Replicate generation timed out')
|
|
668
706
|
if (pred.status === 'failed' || pred.status === 'canceled') {
|
|
669
707
|
throw new Error(`Replicate failed: ${pred.error || 'unknown error'}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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",
|