@goodandready/dsh-image-gen 0.8.1 → 0.8.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/index.js +99 -71
- package/lib/providers.js +15 -8
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
PROVIDER_KEYS,
|
|
24
24
|
buildSidecar,
|
|
25
25
|
makeProviders,
|
|
26
|
+
normalizeCount,
|
|
26
27
|
normalizeMediaType,
|
|
27
28
|
} from './providers.js'
|
|
28
29
|
|
|
@@ -247,7 +248,7 @@ export function apply(ctx, config) {
|
|
|
247
248
|
+ 'Saves the image to the session workspace and shows it in the conversation. '
|
|
248
249
|
+ 'Depending on how the deployment is configured the result carries either the image itself or a link to it; '
|
|
249
250
|
+ 'when you only get a link, answer from the prompt and the link rather than claiming to see the picture. '
|
|
250
|
-
+ 'Use for any text-to-image request.',
|
|
251
|
+
+ 'Use for any text-to-image request. Pass count (1-4) to generate several variations in one call; cost scales with count (default 1).',
|
|
251
252
|
parameters: {
|
|
252
253
|
prompt: {
|
|
253
254
|
type: 'string',
|
|
@@ -271,6 +272,12 @@ export function apply(ctx, config) {
|
|
|
271
272
|
type: 'string',
|
|
272
273
|
description: 'Optional file name stem for the saved image (defaults to a slug of the prompt).',
|
|
273
274
|
},
|
|
275
|
+
count: {
|
|
276
|
+
type: 'integer',
|
|
277
|
+
minimum: 1,
|
|
278
|
+
maximum: 4,
|
|
279
|
+
description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
|
|
280
|
+
},
|
|
274
281
|
},
|
|
275
282
|
output: {
|
|
276
283
|
schema: {
|
|
@@ -296,19 +303,34 @@ export function apply(ctx, config) {
|
|
|
296
303
|
name: { type: 'string' },
|
|
297
304
|
},
|
|
298
305
|
},
|
|
306
|
+
images: {
|
|
307
|
+
type: 'array',
|
|
308
|
+
items: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
additionalProperties: false,
|
|
311
|
+
properties: {
|
|
312
|
+
path: { type: 'string' },
|
|
313
|
+
url: { type: 'string' },
|
|
314
|
+
width: { type: 'integer' },
|
|
315
|
+
height: { type: 'integer' },
|
|
316
|
+
seed: { type: 'integer' },
|
|
317
|
+
prompt: { type: 'string' },
|
|
318
|
+
format: { type: 'string' },
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
299
322
|
},
|
|
300
323
|
},
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// "image": the picture travels in the result itself, which only a
|
|
306
|
-
// vision-capable model — or dsh-vision-bridge — can handle.
|
|
324
|
+
render(args, value) {
|
|
325
|
+
const list = value.images || [value]
|
|
326
|
+
const lines = list.map((img) => `Image generated (${img.width}×${img.height}, ${img.format}, seed ${img.seed}): ${img.path}`)
|
|
327
|
+
const summary = lines.join('\n')
|
|
307
328
|
if (live().deliverAs !== 'image') {
|
|
308
|
-
|
|
329
|
+
const urls = list.map((img) => img.url).filter(Boolean)
|
|
330
|
+
return [{ type: 'text', text: urls.length ? `${summary}\n${urls.join('\n')}` : summary }]
|
|
309
331
|
}
|
|
310
332
|
const blocks = [{ type: 'text', text: summary }]
|
|
311
|
-
if (
|
|
333
|
+
for (const img of list) if (img.attachment) blocks.push({ type: 'image', attachment: img.attachment })
|
|
312
334
|
return blocks
|
|
313
335
|
},
|
|
314
336
|
},
|
|
@@ -335,77 +357,83 @@ export function apply(ctx, config) {
|
|
|
335
357
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
336
358
|
{ prompt: args.prompt, size, format, seed: args.seed, signal: exec.signal },
|
|
337
359
|
)
|
|
338
|
-
const generated = await providers[provider]()
|
|
339
360
|
// Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
|
|
340
361
|
// отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
|
|
341
362
|
// байтами, и пользователь получал битую карточку вместо внятного отказа.
|
|
342
|
-
if (generated && generated.ok === false) throw new Error(generated.reason)
|
|
343
|
-
const
|
|
363
|
+
const guard = (generated) => { if (generated && generated.ok === false) throw new Error(generated.reason) }
|
|
364
|
+
const one = async (jobSeed) => {
|
|
365
|
+
const gen = await providers[provider](jobSeed)
|
|
366
|
+
guard(gen)
|
|
367
|
+
const bytes = gen.bytes
|
|
368
|
+
const mediaType = gen.mediaType
|
|
369
|
+
const extension = mediaType === 'image/jpeg' ? 'jpg' : mediaType === 'image/webp' ? 'webp' : 'png'
|
|
370
|
+
const stem = `${slugify(args.output_name || args.prompt)}-${Date.now().toString(36)}-${jobSeed}`
|
|
371
|
+
const name = `${stem}.${extension}`
|
|
344
372
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
const attachment = await ctx.attachments.saveImage({
|
|
374
|
+
data: new Uint8Array(bytes),
|
|
375
|
+
mediaType,
|
|
376
|
+
name,
|
|
377
|
+
})
|
|
349
378
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
name
|
|
354
|
-
|
|
379
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
380
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir)
|
|
381
|
+
await mkdir(outDir, { recursive: true })
|
|
382
|
+
const filePath = path.join(outDir, name)
|
|
383
|
+
await writeFile(filePath, bytes)
|
|
355
384
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const filePath = path.join(outDir, name)
|
|
362
|
-
await writeFile(filePath, bytes)
|
|
363
|
-
// Sidecar: рядом с картинкой — метаданные генерации. Каталог становится
|
|
364
|
-
// самодокументируемым (галерея/повтор/диагностика читают их без БД).
|
|
365
|
-
await writeFile(
|
|
366
|
-
path.join(outDir, `${stem}.json`),
|
|
367
|
-
JSON.stringify(buildSidecar({
|
|
368
|
-
prompt: args.prompt,
|
|
369
|
-
size,
|
|
370
|
-
format,
|
|
371
|
-
seed: generated.seed,
|
|
372
|
-
provider,
|
|
373
|
-
deliverAs,
|
|
374
|
-
width: generated.width || attachment.width,
|
|
375
|
-
height: generated.height || attachment.height,
|
|
376
|
-
mediaType,
|
|
377
|
-
attachmentId: attachment.attachmentId,
|
|
378
|
-
url: deliverAs === 'image' && generated.sourceUrl ? generated.sourceUrl : localUrl,
|
|
379
|
-
}), null, 2),
|
|
380
|
-
)
|
|
385
|
+
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
386
|
+
+ '&mt=' + encodeURIComponent(attachment.mediaType)
|
|
387
|
+
+ '&b=' + encodeURIComponent(String(attachment.bytes))
|
|
388
|
+
+ '&w=' + encodeURIComponent(String(attachment.width))
|
|
389
|
+
+ '&h=' + encodeURIComponent(String(attachment.height))
|
|
381
390
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
// Sidecar: рядом с картинкой — метаданные генерации. Каталог становится
|
|
392
|
+
// самодокументируемым (галерея/повтор/диагностика читают их без БД).
|
|
393
|
+
await writeFile(
|
|
394
|
+
path.join(outDir, `${stem}.json`),
|
|
395
|
+
JSON.stringify(buildSidecar({
|
|
396
|
+
prompt: args.prompt,
|
|
397
|
+
size,
|
|
398
|
+
format,
|
|
399
|
+
seed: gen.seed,
|
|
400
|
+
provider,
|
|
401
|
+
deliverAs,
|
|
402
|
+
width: gen.width || attachment.width,
|
|
403
|
+
height: gen.height || attachment.height,
|
|
404
|
+
mediaType,
|
|
405
|
+
attachmentId: attachment.attachmentId,
|
|
406
|
+
url: deliverAs === 'image' && gen.sourceUrl ? gen.sourceUrl : localUrl,
|
|
407
|
+
}), null, 2),
|
|
408
|
+
)
|
|
389
409
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
410
|
+
return {
|
|
411
|
+
path: filePath,
|
|
412
|
+
url: deliverAs === 'image' && gen.sourceUrl ? gen.sourceUrl : localUrl,
|
|
413
|
+
width: gen.width || attachment.width,
|
|
414
|
+
height: gen.height || attachment.height,
|
|
415
|
+
seed: gen.seed,
|
|
416
|
+
prompt: args.prompt,
|
|
417
|
+
format: mediaType.replace('image/', ''),
|
|
418
|
+
attachment: {
|
|
419
|
+
attachmentId: attachment.attachmentId,
|
|
420
|
+
mediaType: attachment.mediaType,
|
|
421
|
+
bytes: attachment.bytes,
|
|
422
|
+
width: attachment.width,
|
|
423
|
+
height: attachment.height,
|
|
424
|
+
name: attachment.name,
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const count = normalizeCount(args.count)
|
|
430
|
+
const seedBase = args.seed ?? Math.floor(Math.random() * 100000)
|
|
431
|
+
const images = []
|
|
432
|
+
for (let i = 0; i < count; i += 1) {
|
|
433
|
+
images.push(await one(seedBase + i))
|
|
408
434
|
}
|
|
435
|
+
const first = images[0]
|
|
436
|
+
return { ...first, images }
|
|
409
437
|
},
|
|
410
438
|
}),
|
|
411
439
|
)
|
package/lib/providers.js
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
export const PROVIDER_KEYS = ['fal', 'custom', 'codex', 'grok']
|
|
11
11
|
|
|
12
|
+
/** Привести count из аргумента инструмента к диапазону 1..4. */
|
|
13
|
+
export function normalizeCount(count) {
|
|
14
|
+
const n = Math.floor(Number(count))
|
|
15
|
+
if (!Number.isFinite(n)) return 1
|
|
16
|
+
return Math.max(1, Math.min(4, n))
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
// Размеры, которыми оперируют подписки, — свой набор, не похожий на именованные
|
|
13
20
|
// размеры FAL. Перевод один к одному по смыслу: квадрат, вертикаль, горизонталь.
|
|
14
21
|
export const SUBSCRIPTION_SIZES = {
|
|
@@ -130,10 +137,10 @@ export function makeProviders(deps, job) {
|
|
|
130
137
|
const { fetchImpl, resolveKey, cfg } = deps
|
|
131
138
|
const { prompt, size, format, seed, signal } = job
|
|
132
139
|
|
|
133
|
-
async function fal() {
|
|
140
|
+
async function fal(seedArg = seed) {
|
|
134
141
|
const key = await resolveKey(cfg.apiKeyEnv)
|
|
135
142
|
const body = { prompt, image_size: size, num_images: 1 }
|
|
136
|
-
if (
|
|
143
|
+
if (seedArg !== undefined) body.seed = seedArg
|
|
137
144
|
if (format !== 'png') body.output_format = format
|
|
138
145
|
|
|
139
146
|
const submit = await submitJob(fetchImpl, cfg.baseURL, cfg.model, key, body, signal)
|
|
@@ -158,13 +165,13 @@ export function makeProviders(deps, job) {
|
|
|
158
165
|
mediaType: normalizeMediaType(image.content_type, format),
|
|
159
166
|
width: image.width ?? 0,
|
|
160
167
|
height: image.height ?? 0,
|
|
161
|
-
seed: result.seed ??
|
|
168
|
+
seed: result.seed ?? seedArg ?? 0,
|
|
162
169
|
sourceUrl: image.url,
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
// Любой OpenAI-совместимый API картинок. Один запрос вместо очереди FAL.
|
|
167
|
-
async function custom() {
|
|
174
|
+
async function custom(seedArg = seed) {
|
|
168
175
|
const base = String(cfg.customBaseURL || '').replace(/\/+$/, '')
|
|
169
176
|
if (!base) throw new Error('Custom image provider: base URL is not configured (Settings → Image generation)')
|
|
170
177
|
if (!cfg.customModel) throw new Error('Custom image provider: model is not configured')
|
|
@@ -204,7 +211,7 @@ export function makeProviders(deps, job) {
|
|
|
204
211
|
mediaType: normalizeMediaType(data.output_format || '', format),
|
|
205
212
|
width: 0,
|
|
206
213
|
height: 0,
|
|
207
|
-
seed:
|
|
214
|
+
seed: seedArg ?? 0,
|
|
208
215
|
sourceUrl: '',
|
|
209
216
|
}
|
|
210
217
|
}
|
|
@@ -223,7 +230,7 @@ export function makeProviders(deps, job) {
|
|
|
223
230
|
mediaType: normalizeMediaType(contentType, format),
|
|
224
231
|
width: 0,
|
|
225
232
|
height: 0,
|
|
226
|
-
seed:
|
|
233
|
+
seed: seedArg ?? 0,
|
|
227
234
|
sourceUrl: item.url,
|
|
228
235
|
}
|
|
229
236
|
}
|
|
@@ -232,7 +239,7 @@ export function makeProviders(deps, job) {
|
|
|
232
239
|
// только запрос и разбор ответа. Токен сюда не попадает вовсе — служба
|
|
233
240
|
// отдаёт готовую картинку, а не ключ доступа.
|
|
234
241
|
function subscription(provider) {
|
|
235
|
-
return async function generate() {
|
|
242
|
+
return async function generate(seedArg = seed) {
|
|
236
243
|
const images = deps.subscriptionImages
|
|
237
244
|
if (!images || typeof images.generate !== 'function') {
|
|
238
245
|
return {
|
|
@@ -263,7 +270,7 @@ export function makeProviders(deps, job) {
|
|
|
263
270
|
mediaType: 'image/png',
|
|
264
271
|
width: 0,
|
|
265
272
|
height: 0,
|
|
266
|
-
seed:
|
|
273
|
+
seed: seedArg ?? 0,
|
|
267
274
|
sourceUrl: '',
|
|
268
275
|
revisedPrompt: first.revisedPrompt || '',
|
|
269
276
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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",
|