@goodandready/dsh-image-gen 0.8.5 → 0.8.6
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 +70 -6
- package/lib/index.js +18 -4
- package/lib/providers.js +29 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -99,7 +99,7 @@ window.__ModuleLoader__.load({
|
|
|
99
99
|
function PluginSettingsCard(props) {
|
|
100
100
|
// Карточка живёт в собственном разделе настроек и на странице одна, так
|
|
101
101
|
// что открыта сразу: свёрнутая выглядела бы как пустая страница.
|
|
102
|
-
const [open, setOpen] = react.useState(
|
|
102
|
+
const [open, setOpen] = react.useState(false)
|
|
103
103
|
const { state } = props
|
|
104
104
|
// Раньше здесь было `return null`, и раздел оказывался пустым, если хост
|
|
105
105
|
// ещё не объявил настройки. Пустая страница ничего не объясняет — лучше
|
|
@@ -407,6 +407,16 @@ window.__ModuleLoader__.load({
|
|
|
407
407
|
|
|
408
408
|
// ------------------------------------------------------------- fields
|
|
409
409
|
const IMAGE_SIZES = ['square_hd', 'square', 'portrait_4_3', 'portrait_16_9', 'landscape_4_3', 'landscape_16_9']
|
|
410
|
+
// Перевод именованного размера в пиксели для подсказки в карточке
|
|
411
|
+
// (значения совпадают с SIZE_PIXELS в хостовой половине).
|
|
412
|
+
const SIZE_PIXELS = {
|
|
413
|
+
square_hd: '1024x1024',
|
|
414
|
+
square: '512x512',
|
|
415
|
+
portrait_4_3: '768x1024',
|
|
416
|
+
portrait_16_9: '576x1024',
|
|
417
|
+
landscape_4_3: '1024x768',
|
|
418
|
+
landscape_16_9: '1024x576',
|
|
419
|
+
}
|
|
410
420
|
const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
411
421
|
const DELIVERY_MODES = ['link', 'image']
|
|
412
422
|
const PROVIDERS = ['fal', 'custom', 'codex', 'grok']
|
|
@@ -417,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
417
427
|
{ field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model' },
|
|
418
428
|
{ field: 'apiKeyEnv', spec: textField('apiKeyEnv'), kind: 'text', labelKey: 'f.apiKeyEnv', hintKey: 'f.apiKeyEnvHint', placeholderKey: 'p.apiKeyEnv' },
|
|
419
429
|
{ field: 'baseURL', spec: textField('baseURL'), kind: 'text', labelKey: 'f.baseURL', hintKey: 'f.baseURLHint', placeholderKey: 'p.baseURL' },
|
|
420
|
-
{ field: 'defaultSize', spec: selectField('defaultSize', IMAGE_SIZES), kind: 'select', options: IMAGE_SIZES, labelKey: 'f.defaultSize', hintKey: 'f.defaultSizeHint' },
|
|
430
|
+
{ field: 'defaultSize', spec: selectField('defaultSize', IMAGE_SIZES), kind: 'select', options: IMAGE_SIZES, labelKey: 'f.defaultSize', hintKey: 'f.defaultSizeHint', sizePixels: SIZE_PIXELS },
|
|
421
431
|
{ field: 'defaultFormat', spec: selectField('defaultFormat', OUTPUT_FORMATS), kind: 'select', options: OUTPUT_FORMATS, labelKey: 'f.defaultFormat', hintKey: 'f.defaultFormatHint' },
|
|
422
432
|
{ field: 'pollIntervalMs', spec: numberField('pollIntervalMs'), kind: 'number', labelKey: 'f.pollIntervalMs', hintKey: 'f.pollIntervalMsHint', placeholderKey: 'p.pollIntervalMs' },
|
|
423
433
|
{ field: 'timeoutMs', spec: numberField('timeoutMs'), kind: 'number', labelKey: 'f.timeoutMs', hintKey: 'f.timeoutMsHint', placeholderKey: 'p.timeoutMs' },
|
|
@@ -446,7 +456,9 @@ window.__ModuleLoader__.load({
|
|
|
446
456
|
onReset,
|
|
447
457
|
}
|
|
448
458
|
if (entry.kind === 'select') {
|
|
449
|
-
|
|
459
|
+
const px = entry.sizePixels && entry.sizePixels[state.text] ? ` — ${entry.sizePixels[state.text]}` : ''
|
|
460
|
+
const hint = base.hint + px
|
|
461
|
+
return react_jsx_runtime.jsx(SelectField, { ...base, hint, options: entry.options, inheritLabel: t('f.inherit') })
|
|
450
462
|
}
|
|
451
463
|
return react_jsx_runtime.jsx(ValueField, { ...base, numeric: entry.kind === 'number', placeholder: entry.placeholderKey ? t(entry.placeholderKey) : '' })
|
|
452
464
|
}
|
|
@@ -511,7 +523,7 @@ window.__ModuleLoader__.load({
|
|
|
511
523
|
/** Settings namespace the card edits (the Host plugin registers it). */
|
|
512
524
|
const SETTINGS_NS = 'dsh-image-gen'
|
|
513
525
|
/** Required client services. */
|
|
514
|
-
const inject = ['slots', 'settingsScope', 'locale']
|
|
526
|
+
const inject = ['slots', 'settingsScope', 'locale', 'sessions']
|
|
515
527
|
|
|
516
528
|
const en = {
|
|
517
529
|
'settings.navLabel': 'Image generation',
|
|
@@ -636,7 +648,7 @@ window.__ModuleLoader__.load({
|
|
|
636
648
|
// Карточки инструментов не рисуют image-блоки (это умеют только сообщения
|
|
637
649
|
// ассистента), поэтому сгенерированная картинка показывается здесь —
|
|
638
650
|
// keyed-запись слота tool.call.toolview по имени инструмента.
|
|
639
|
-
const CARD_CSS = '.fal-card{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);max-width:520px}'
|
|
651
|
+
const CARD_CSS = '.fal-card{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);max-width:520px}' + '.fal-repeat{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:5px 14px;font-size:13px}' + '.fal-repeat:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}' + '.fal-repeat:disabled{opacity:.5;cursor:default}'
|
|
640
652
|
+ '.fal-card .fal-head{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}'
|
|
641
653
|
+ '.fal-card img{max-width:100%;border-radius:8px;display:block}'
|
|
642
654
|
+ '.fal-card .fal-prompt{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}'
|
|
@@ -697,6 +709,37 @@ window.__ModuleLoader__.load({
|
|
|
697
709
|
const head = react.createElement('div', { className: 'fal-head' },
|
|
698
710
|
running ? 'Генерирую изображение…' : failed ? 'Генерация не удалась' : 'Изображение')
|
|
699
711
|
|
|
712
|
+
// «Повторить генерацию»: отправляет в чат новое сообщение с теми же
|
|
713
|
+
// промптом и параметрами, извлечёнными из исходного вызова инструмента.
|
|
714
|
+
// Дальше модель сама решает, как сгенерировать, — это просто повторный
|
|
715
|
+
// запрос, а не вызов инструмента из карточки.
|
|
716
|
+
let args = {}
|
|
717
|
+
try {
|
|
718
|
+
const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
|
|
719
|
+
if (raw) args = JSON.parse(raw)
|
|
720
|
+
} catch (e) { /* ignore */ }
|
|
721
|
+
const repeatText = (() => {
|
|
722
|
+
const parts = [String(args.prompt || '').trim()]
|
|
723
|
+
const size = args.image_size || args.size
|
|
724
|
+
const seed = args.seed
|
|
725
|
+
if (size) parts.push(`Размер: ${size}`)
|
|
726
|
+
if (seed !== undefined) parts.push(`Сид: ${seed}`)
|
|
727
|
+
return parts.filter(Boolean).join(', ')
|
|
728
|
+
})()
|
|
729
|
+
const repeat = async () => {
|
|
730
|
+
try {
|
|
731
|
+
const sessions = props.sessions
|
|
732
|
+
const current = sessions && sessions.list && sessions.list.current
|
|
733
|
+
const binding = current && sessions.binding(current)
|
|
734
|
+
const session = binding && binding.session
|
|
735
|
+
if (!session || !session.prompt) {
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
const text = repeatText || String(args.prompt || '')
|
|
739
|
+
await session.prompt([{ type: 'text', text }], 'queue')
|
|
740
|
+
} catch (e) { /* контур сам покажет ошибку отправки */ }
|
|
741
|
+
}
|
|
742
|
+
|
|
700
743
|
const body = []
|
|
701
744
|
if (prompt) body.push(react.createElement('div', { className: 'fal-prompt', key: 'p' }, prompt))
|
|
702
745
|
if (failed) {
|
|
@@ -729,6 +772,27 @@ window.__ModuleLoader__.load({
|
|
|
729
772
|
body.push(react.createElement('div', { className: 'fal-meta', key: 'm' }, parsed.text))
|
|
730
773
|
}
|
|
731
774
|
|
|
775
|
+
// Диагностика: метаданные генерации (prompt/seed/size/format), чтобы было
|
|
776
|
+
// понятно, по каким параметрам получилась именно эта картинка.
|
|
777
|
+
const metaParts = []
|
|
778
|
+
if (args.image_size || args.size) metaParts.push(`size ${args.image_size || args.size}`)
|
|
779
|
+
if (args.output_format || args.format) metaParts.push(`format ${args.output_format || args.format}`)
|
|
780
|
+
if (args.seed !== undefined) metaParts.push(`seed ${args.seed}`)
|
|
781
|
+
const meta = metaParts.join(' · ')
|
|
782
|
+
|
|
783
|
+
// Кнопка повтора — только когда генерация завершена (не в процессе и не ошибка).
|
|
784
|
+
if (!running && !failed && meta) {
|
|
785
|
+
body.push(react.createElement('div', { key: 'meta', className: 'fal-meta' }, meta))
|
|
786
|
+
}
|
|
787
|
+
if (!running && !failed && repeatText) {
|
|
788
|
+
body.push(react.createElement('button', {
|
|
789
|
+
key: 'repeat',
|
|
790
|
+
type: 'button',
|
|
791
|
+
className: 'fal-repeat',
|
|
792
|
+
onClick: repeat,
|
|
793
|
+
}, 'Повторить генерацию'))
|
|
794
|
+
}
|
|
795
|
+
|
|
732
796
|
return react.createElement('div', { className: 'fal-card' }, head, body)
|
|
733
797
|
}
|
|
734
798
|
|
|
@@ -810,7 +874,7 @@ window.__ModuleLoader__.load({
|
|
|
810
874
|
}
|
|
811
875
|
ctx.slots.inject('tool.call.toolview', () =>
|
|
812
876
|
ctx.slots.register(
|
|
813
|
-
{ name: 'tool.call.toolview', key: 'generate_image', locale: NS },
|
|
877
|
+
{ name: 'tool.call.toolview', key: 'generate_image', locale: NS, inject: () => ({ sessions: ctx.sessions }) },
|
|
814
878
|
FalImageCard,
|
|
815
879
|
),
|
|
816
880
|
)
|
package/lib/index.js
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
buildSidecar,
|
|
25
25
|
makeProviders,
|
|
26
26
|
normalizeCount,
|
|
27
|
+
tryGenerate,
|
|
28
|
+
fallbackOrder,
|
|
27
29
|
normalizeMediaType,
|
|
28
30
|
} from './providers.js'
|
|
29
31
|
|
|
@@ -276,6 +278,14 @@ export function apply(ctx, config) {
|
|
|
276
278
|
type: 'integer',
|
|
277
279
|
description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
|
|
278
280
|
},
|
|
281
|
+
negative_prompt: {
|
|
282
|
+
type: 'string',
|
|
283
|
+
description: 'Optional text describing what NOT to draw; sent to providers that support it (ignored otherwise).',
|
|
284
|
+
},
|
|
285
|
+
guidance_scale: {
|
|
286
|
+
type: 'number',
|
|
287
|
+
description: 'Optional prompt adherence (e.g. 1-20); sent to providers that support it (ignored otherwise).',
|
|
288
|
+
},
|
|
279
289
|
},
|
|
280
290
|
output: {
|
|
281
291
|
schema: {
|
|
@@ -353,14 +363,14 @@ export function apply(ctx, config) {
|
|
|
353
363
|
|
|
354
364
|
const providers = makeProviders(
|
|
355
365
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
356
|
-
{ prompt: args.prompt, size, format, seed: args.seed, signal: exec.signal },
|
|
366
|
+
{ prompt: args.prompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: args.negative_prompt, guidanceScale: args.guidance_scale },
|
|
357
367
|
)
|
|
358
368
|
// Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
|
|
359
369
|
// отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
|
|
360
370
|
// байтами, и пользователь получал битую карточку вместо внятного отказа.
|
|
361
371
|
const guard = (generated) => { if (generated && generated.ok === false) throw new Error(generated.reason) }
|
|
362
|
-
const one = async (jobSeed) => {
|
|
363
|
-
const gen = await providers[
|
|
372
|
+
const one = async (jobSeed, providerKey) => {
|
|
373
|
+
const gen = await providers[providerKey](jobSeed)
|
|
364
374
|
guard(gen)
|
|
365
375
|
const bytes = gen.bytes
|
|
366
376
|
const mediaType = gen.mediaType
|
|
@@ -426,9 +436,13 @@ export function apply(ctx, config) {
|
|
|
426
436
|
|
|
427
437
|
const count = normalizeCount(args.count)
|
|
428
438
|
const seedBase = args.seed ?? Math.floor(Math.random() * 100000)
|
|
439
|
+
// Fallback-цепочка: пробуем текущий провайдер, при отказе — следующий
|
|
440
|
+
// по порядку (fal → custom → codex → grok), собирая причины отказов.
|
|
441
|
+
const order = fallbackOrder(provider)
|
|
442
|
+
const generators = Object.fromEntries(PROVIDER_KEYS.map((k) => [k, (s) => one(s, k)]))
|
|
429
443
|
const images = []
|
|
430
444
|
for (let i = 0; i < count; i += 1) {
|
|
431
|
-
images.push(await
|
|
445
|
+
images.push(await tryGenerate(generators, order, seedBase + i))
|
|
432
446
|
}
|
|
433
447
|
const first = images[0]
|
|
434
448
|
return { ...first, images }
|
package/lib/providers.js
CHANGED
|
@@ -10,6 +10,30 @@
|
|
|
10
10
|
export const PROVIDER_KEYS = ['fal', 'custom', 'codex', 'grok']
|
|
11
11
|
|
|
12
12
|
/** Привести count из аргумента инструмента к диапазону 1..4. */
|
|
13
|
+
/** Порядок провайдеров для fallback: основной первым, остальные по PROVIDER_KEYS. */
|
|
14
|
+
export function fallbackOrder(primary) {
|
|
15
|
+
return [primary, ...PROVIDER_KEYS.filter((k) => k !== primary)]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Перебирает генераторов по порядку, возвращает первый успешный результат.
|
|
20
|
+
* При отказе всех бросает с перечислением причин.
|
|
21
|
+
* @param generators - массив функций (key, seed) => Promise<generated>.
|
|
22
|
+
* @param order - порядок ключей, длина = generators.length.
|
|
23
|
+
*/
|
|
24
|
+
export async function tryGenerate(generators, order, seed) {
|
|
25
|
+
const refusals = []
|
|
26
|
+
for (const key of order) {
|
|
27
|
+
try {
|
|
28
|
+
const produced = await generators[key](seed)
|
|
29
|
+
return produced
|
|
30
|
+
} catch (e) {
|
|
31
|
+
refusals.push(`${key}: ${e.message || String(e)}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Image generation failed on all providers. ${refusals.join('; ')}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
export function normalizeCount(count) {
|
|
14
38
|
const n = Math.floor(Number(count))
|
|
15
39
|
if (!Number.isFinite(n)) return 1
|
|
@@ -135,12 +159,14 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
|
|
|
135
159
|
*/
|
|
136
160
|
export function makeProviders(deps, job) {
|
|
137
161
|
const { fetchImpl, resolveKey, cfg } = deps
|
|
138
|
-
const { prompt, size, format, seed, signal } = job
|
|
162
|
+
const { prompt, size, format, seed, signal, negativePrompt, guidanceScale } = job
|
|
139
163
|
|
|
140
164
|
async function fal(seedArg = seed) {
|
|
141
165
|
const key = await resolveKey(cfg.apiKeyEnv)
|
|
142
166
|
const body = { prompt, image_size: size, num_images: 1 }
|
|
143
167
|
if (seedArg !== undefined) body.seed = seedArg
|
|
168
|
+
if (negativePrompt !== undefined) body.negative_prompt = negativePrompt
|
|
169
|
+
if (guidanceScale !== undefined) body.guidance_scale = guidanceScale
|
|
144
170
|
if (format !== 'png') body.output_format = format
|
|
145
171
|
|
|
146
172
|
const submit = await submitJob(fetchImpl, cfg.baseURL, cfg.model, key, body, signal)
|
|
@@ -192,6 +218,8 @@ export function makeProviders(deps, job) {
|
|
|
192
218
|
prompt,
|
|
193
219
|
n: 1,
|
|
194
220
|
size: cfg.customSize || SIZE_PIXELS[size] || size,
|
|
221
|
+
...(negativePrompt !== undefined ? { negative_prompt: negativePrompt } : {}),
|
|
222
|
+
...(guidanceScale !== undefined ? { guidance_scale: guidanceScale } : {}),
|
|
195
223
|
}),
|
|
196
224
|
signal,
|
|
197
225
|
})
|
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.6",
|
|
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",
|