@goodandready/dsh-image-gen 0.10.7 → 0.10.10
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/README.md +13 -0
- package/README.ru.md +13 -0
- package/lib/client.js +147 -108
- package/lib/index.js +57 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
## 🚀 Updates v0.10.8: Lossless JSON Unification, Dual-Output & Strict Validation (#199)
|
|
3
|
+
- **Lossless JSON & Dual-Output Unification**: `upscale_image`, `remove_background`, `blend_images`, and `vectorize_image` now consistently return `toLosslessJson` and formatted markdown `summary` for text-only LLMs.
|
|
4
|
+
- **Strict Input Image Validation**: `extract_design_tokens`, `image_to_css_gradient`, and `check_image_contrast` explicitly validate source image readability instead of silent fallback.
|
|
5
|
+
- **Unit Testing Suite**: Added `test/tool-consistency.test.mjs` covering tokenization, gradient generator, and PWA suite.
|
|
6
|
+
|
|
1
7
|
# 📦 @goodandready/dsh-image-gen
|
|
2
8
|
|
|
3
9
|
<div align="center">
|
|
@@ -77,6 +83,13 @@
|
|
|
77
83
|
* **FAL Credential Aliasing**: seamless fallback between `FAL_API_KEY` and `FAL_KEY` in credentials and environment.
|
|
78
84
|
* **Subscription Aspect Ratio Mapping**: maps aspect ratios (`16:9`, `3:2`, `9:16`, `2:3`) to appropriate subscription dimensions (`1536x1024` / `1024x1536`) instead of falling back to default square `1024x1024`.
|
|
79
85
|
|
|
86
|
+
|
|
87
|
+
### 🚀 What's New in v0.10.10 (#201, #203)
|
|
88
|
+
* **Settings GUI Stabilization (#201)**: Fixed `booleanField` spec in client runtime that prevented the settings configuration pane from rendering in DSH Web UI. All configuration fields (providers, model identifiers, API credentials, style presets, LLM enhancer, timeouts, cache retention) render cleanly.
|
|
89
|
+
* **Streamlined Settings UI (#203)**: Removed the bulky in-settings history gallery to keep the configuration panel focused, fast, and organized.
|
|
90
|
+
* **Comprehensive Localization (#201)**: Added 100% dictionary coverage for all provider credentials, endpoints, and field hints in English and Russian.
|
|
91
|
+
* **Robust Slot Mounting**: Implemented `registerFirst` helper with graceful fallback between `settings.plugin.item` and `settings.section`.
|
|
92
|
+
|
|
80
93
|
## 🎨 Supported Generation Backends
|
|
81
94
|
|
|
82
95
|
* **`fal`** (Default): FAL.ai queue for FLUX.1, SDXL, Clarity Upscaler, and BiRefNet.
|
package/README.ru.md
CHANGED
|
@@ -116,6 +116,12 @@ graph LR
|
|
|
116
116
|
---
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
|
|
120
|
+
## 🚀 Обновления v0.10.8: унификация Lossless JSON, Dual-Output и строгая валидация (#199)
|
|
121
|
+
- **Унификация Lossless JSON и Dual-Output**: Инструменты `upscale_image`, `remove_background`, `blend_images`, `vectorize_image` теперь гарантированно возвращают валидный `toLosslessJson` и генерируют структурированный `summary` с превью для текстовых LLM.
|
|
122
|
+
- **Строгая валидация входных изображений**: В инструментах `extract_design_tokens`, `image_to_css_gradient` и `check_image_contrast` добавлена явная проверка существования файла с информативной ошибкой (`Image not found or unreadable: <path>`).
|
|
123
|
+
- **Сквозное модульное тестирование**: Добавлен тест-сьют `test/tool-consistency.test.mjs` для проверки отказоустойчивости дизайн- и PWA-инструментов.
|
|
124
|
+
|
|
119
125
|
## 🚀 Обновления v0.10.4: жизненный цикл Cordis, полнота настроек и интернационализация
|
|
120
126
|
- **Жизненный цикл Cordis (#136)**: регистрация всех 7 инструментов обёрнута в `ctx.effect` для штатной отписки при перезагрузке плагина.
|
|
121
127
|
- **Полнота настроек в GUI (#137)**: в карточку настроек добавлены поля для Replicate, SeaDream, Gemini, Local ComfyUI/A1111, пресетов стилей и LLM-улучшителя.
|
|
@@ -123,6 +129,13 @@ graph LR
|
|
|
123
129
|
- **Оптимизация компонентов (#139)**: из `FalImageCard` удалён неиспользуемый мёртвый стейт.
|
|
124
130
|
- **100% интернационализация (#140)**: устранены захардкоженные русские строки, добавлены переводы на русский, английский и китайский языки.
|
|
125
131
|
|
|
132
|
+
|
|
133
|
+
### 🚀 Что нового в v0.10.10 (#201, #203)
|
|
134
|
+
* **Стабилизация окна настроек GUI (#201)**: Исправлена спецификация `booleanField` в клиентском коде, вызывавшая падение рендера карточки настроек плагина в DSH Web UI. Форма настроек теперь открывается штатно со всеми полями, селекторами провайдеров, моделями, ссылками на ключи и пресетами.
|
|
135
|
+
* **Очистка интерфейса настроек (#203)**: Из окна настроек убрана встроенная галерея истории генераций, загромождавшая карточку настроек.
|
|
136
|
+
* **Полная локализация (#201)**: Добавлены все недостающие строки переводов и подсказок на русском и английском языках для всех поддерживаемых провайдеров.
|
|
137
|
+
* **Надёжная регистрация слотов**: Реализован `registerFirst` с плавным откатом между `settings.plugin.item` и `settings.section`.
|
|
138
|
+
|
|
126
139
|
## 🎨 Поддерживаемые провайдеры генерации
|
|
127
140
|
|
|
128
141
|
* **`fal`** *(дефолт)*: Сверхбыстрая очередь FAL.ai (`FLUX.1-schnell`, `FLUX-dev`, `SDXL`, BiRefNet, Clarity Upscaler).
|
package/lib/client.js
CHANGED
|
@@ -82,12 +82,7 @@ window.__ModuleLoader__.load({
|
|
|
82
82
|
'.fal_input:disabled,.fal_select:disabled{opacity:.6}' +
|
|
83
83
|
'.fal_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}' +
|
|
84
84
|
'.fal_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}' +
|
|
85
|
-
'.fal_sep{height:1px;background:var(--dsw-alias-border-l2);margin:2px 0 0}'
|
|
86
|
-
'.fal_gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-top:12px}' +
|
|
87
|
-
'.fal_galleryItem{display:flex;flex-direction:column;gap:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}' +
|
|
88
|
-
'.fal_galleryImg{width:100%;height:90px;object-fit:cover;display:block}' +
|
|
89
|
-
'.fal_galleryMeta{font-size:11px;color:var(--dsw-alias-label-secondary);padding:4px 6px}' +
|
|
90
|
-
'.fal_galleryEmpty{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:12px}'
|
|
85
|
+
'.fal_sep{height:1px;background:var(--dsw-alias-border-l2);margin:2px 0 0}'
|
|
91
86
|
const tagId = 'dsh-image-gen/settings-card.module.css'
|
|
92
87
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
|
|
93
88
|
const tag = document.createElement('style')
|
|
@@ -124,11 +119,6 @@ window.__ModuleLoader__.load({
|
|
|
124
119
|
save: 'fal_save',
|
|
125
120
|
select: 'fal_select',
|
|
126
121
|
sep: 'fal_sep',
|
|
127
|
-
gallery: 'fal_gallery',
|
|
128
|
-
galleryItem: 'fal_galleryItem',
|
|
129
|
-
galleryImg: 'fal_galleryImg',
|
|
130
|
-
galleryMeta: 'fal_galleryMeta',
|
|
131
|
-
galleryEmpty: 'fal_galleryEmpty',
|
|
132
122
|
}
|
|
133
123
|
|
|
134
124
|
// -------------------------------------------------------------- chrome
|
|
@@ -136,7 +126,7 @@ window.__ModuleLoader__.load({
|
|
|
136
126
|
function PluginSettingsCard(props) {
|
|
137
127
|
// Карточка живёт в собственном разделе настроек и на странице одна, так
|
|
138
128
|
// что открыта сразу: свёрнутая выглядела бы как пустая страница.
|
|
139
|
-
const [open, setOpen] = react.useState(
|
|
129
|
+
const [open, setOpen] = react.useState(props.defaultOpen ?? true)
|
|
140
130
|
const { state } = props
|
|
141
131
|
// Раньше здесь было `return null`, и раздел оказывался пустым, если хост
|
|
142
132
|
// ещё не объявил настройки. Пустая страница ничего не объясняет — лучше
|
|
@@ -464,7 +454,17 @@ window.__ModuleLoader__.load({
|
|
|
464
454
|
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
465
455
|
|
|
466
456
|
/** Field specs in display order. */
|
|
467
|
-
const booleanField = (
|
|
457
|
+
const booleanField = (field) => ({
|
|
458
|
+
field,
|
|
459
|
+
type: 'boolean',
|
|
460
|
+
format: (value) => (typeof value === 'boolean' ? String(value) : (value === 'true' || value === 'false' ? value : '')),
|
|
461
|
+
parse: (text) => {
|
|
462
|
+
if (text === '') return { kind: 'clear' }
|
|
463
|
+
if (text === 'true') return { kind: 'set', value: true }
|
|
464
|
+
if (text === 'false') return { kind: 'set', value: false }
|
|
465
|
+
return void 0
|
|
466
|
+
},
|
|
467
|
+
})
|
|
468
468
|
const STYLE_PRESET_OPTIONS = ['', 'cinematic', 'anime', 'digital_art', 'photographic', 'fantasy', 'isometric', 'cyberpunk', 'watercolor', 'origami', 'oil_painting', 'pixel_art', 'logo']
|
|
469
469
|
|
|
470
470
|
const FIELDS = [
|
|
@@ -511,7 +511,6 @@ window.__ModuleLoader__.load({
|
|
|
511
511
|
{ field: 'deliverAs', spec: selectField('deliverAs', DELIVERY_MODES), kind: 'select', options: DELIVERY_MODES, labelKey: 'f.deliverAs', hintKey: 'f.deliverAsHint' },
|
|
512
512
|
{ field: 'outputDir', spec: textField('outputDir'), kind: 'text', labelKey: 'f.outputDir', hintKey: 'f.outputDirHint', placeholderKey: 'p.outputDir' },
|
|
513
513
|
{ field: 'cacheTtlDays', spec: numberField('cacheTtlDays'), kind: 'number', labelKey: 'f.cacheTtlDays', hintKey: 'f.cacheTtlDaysHint', placeholderKey: 'p.cacheTtlDays' },
|
|
514
|
-
{ field: 'maxHistoryEntries', spec: numberField('maxHistoryEntries'), kind: 'number', labelKey: 'f.maxHistoryEntries', hintKey: 'f.maxHistoryEntriesHint', placeholderKey: 'p.maxHistoryEntries' },
|
|
515
514
|
]
|
|
516
515
|
|
|
517
516
|
function Field(props) {
|
|
@@ -539,66 +538,11 @@ window.__ModuleLoader__.load({
|
|
|
539
538
|
return react_jsx_runtime.jsx(ValueField, { ...base, numeric: entry.kind === 'number', placeholder: entry.placeholderKey ? t(entry.placeholderKey) : '' })
|
|
540
539
|
}
|
|
541
540
|
|
|
542
|
-
/** Render the plugin settings card. */
|
|
543
|
-
/** Галерея истории генераций: тянет /history и рендерит миниатюры. */
|
|
544
|
-
function HistoryGallery(props) {
|
|
545
|
-
const [entries, setEntries] = react.useState([])
|
|
546
|
-
const [loading, setLoading] = react.useState(true)
|
|
547
|
-
const [filter, setFilter] = react.useState('')
|
|
548
|
-
react.useEffect(() => {
|
|
549
|
-
let cancelled = false
|
|
550
|
-
fetch('/dsh-image-gen/history')
|
|
551
|
-
.then((r) => r.json())
|
|
552
|
-
.then((data) => { if (!cancelled) { setEntries(Array.isArray(data) ? data : []); setLoading(false) } })
|
|
553
|
-
.catch(() => { if (!cancelled) setLoading(false) })
|
|
554
|
-
return () => { cancelled = true }
|
|
555
|
-
}, [])
|
|
556
|
-
const t = props.t
|
|
557
|
-
if (loading) return react_jsx_runtime.jsx('p', { className: cssDefault.galleryEmpty, children: t('history.loading') })
|
|
558
|
-
if (!entries.length) return react_jsx_runtime.jsx('p', { className: cssDefault.galleryEmpty, children: t('history.empty') })
|
|
559
|
-
const providers = [...new Set(entries.map((e) => e.provider).filter(Boolean))]
|
|
560
|
-
const filtered = filter ? entries.filter((e) => e.provider === filter) : entries
|
|
561
|
-
return react_jsx_runtime.jsxs('div', {
|
|
562
|
-
className: cssDefault.gallery,
|
|
563
|
-
children: [
|
|
564
|
-
react_jsx_runtime.jsx('select', {
|
|
565
|
-
className: cssDefault.select,
|
|
566
|
-
value: filter,
|
|
567
|
-
onChange: (e) => setFilter(e.target.value),
|
|
568
|
-
children: [
|
|
569
|
-
react_jsx_runtime.jsx('option', { value: '', children: t('history.all') }),
|
|
570
|
-
providers.map((p) => react_jsx_runtime.jsx('option', { value: p, children: p }, p)),
|
|
571
|
-
],
|
|
572
|
-
}),
|
|
573
|
-
filtered.map((e) =>
|
|
574
|
-
react_jsx_runtime.jsxs('a', {
|
|
575
|
-
className: cssDefault.galleryItem,
|
|
576
|
-
key: e.path,
|
|
577
|
-
href: e.thumbnailUrl || '#',
|
|
578
|
-
target: '_blank',
|
|
579
|
-
rel: 'noreferrer',
|
|
580
|
-
children: [
|
|
581
|
-
e.thumbnailUrl ? react_jsx_runtime.jsx('img', { className: cssDefault.galleryImg, src: e.thumbnailUrl, alt: e.prompt || '', loading: 'lazy' }) : null,
|
|
582
|
-
react_jsx_runtime.jsx('div', { className: cssDefault.galleryMeta, children: `${e.provider || ''} · ${e.size || ''}` }),
|
|
583
|
-
e.seed !== undefined ? react_jsx_runtime.jsx('button', {
|
|
584
|
-
type: 'button',
|
|
585
|
-
className: 'fal-repeat',
|
|
586
|
-
onClick: (ev) => {
|
|
587
|
-
ev.preventDefault()
|
|
588
|
-
ev.stopPropagation()
|
|
589
|
-
try { navigator.clipboard.writeText(String(e.seed)) } catch (err) { /* нет доступа к буферу */ }
|
|
590
|
-
},
|
|
591
|
-
}, `seed ${e.seed}`) : null,
|
|
592
|
-
],
|
|
593
|
-
}),
|
|
594
|
-
),
|
|
595
|
-
],
|
|
596
|
-
})
|
|
597
|
-
}
|
|
598
|
-
|
|
599
541
|
function FalSettingsCard(props) {
|
|
600
|
-
const
|
|
601
|
-
const state = props.useFalSettingsCard
|
|
542
|
+
const t = props.t || ((k) => k)
|
|
543
|
+
const state = (typeof props.useFalSettingsCard === 'function'
|
|
544
|
+
? props.useFalSettingsCard((snapshot) => snapshot)
|
|
545
|
+
: null) || props.state || { available: true, writable: true, provider: { text: 'fal' } }
|
|
602
546
|
const disabled = !state.writable
|
|
603
547
|
const currentProvider = (state.provider && state.provider.text) || 'fal'
|
|
604
548
|
const visibleFields = FIELDS.filter((entry) => !entry.when || entry.when.includes(currentProvider))
|
|
@@ -616,7 +560,7 @@ window.__ModuleLoader__.load({
|
|
|
616
560
|
onSave: props.save,
|
|
617
561
|
onDiscard: props.discard,
|
|
618
562
|
children: [
|
|
619
|
-
react_jsx_runtime.jsx('p', { className: cssDefault.hint, children: t('settings.credentialHint') }),
|
|
563
|
+
react_jsx_runtime.jsx('p', { className: cssDefault.hint, key: 'credHint', children: t('settings.credentialHint') }),
|
|
620
564
|
visibleFields.map((entry) =>
|
|
621
565
|
react_jsx_runtime.jsx(
|
|
622
566
|
Field,
|
|
@@ -625,15 +569,12 @@ window.__ModuleLoader__.load({
|
|
|
625
569
|
state: state[entry.field] ?? { text: '', overridden: false, invalid: false },
|
|
626
570
|
fieldProps,
|
|
627
571
|
t,
|
|
628
|
-
onEdit: (text) => props.edit(entry.field, text),
|
|
629
|
-
onReset: () => props.resetField(entry.field),
|
|
572
|
+
onEdit: (text) => props.edit && props.edit(entry.field, text),
|
|
573
|
+
onReset: () => props.resetField && props.resetField(entry.field),
|
|
630
574
|
},
|
|
631
575
|
entry.field,
|
|
632
576
|
),
|
|
633
577
|
),
|
|
634
|
-
react_jsx_runtime.jsx('div', { className: cssDefault.sep, key: 'hsep' }),
|
|
635
|
-
react_jsx_runtime.jsx('p', { className: cssDefault.hint, key: 'hlabel', children: t('history.title') }),
|
|
636
|
-
react_jsx_runtime.jsx(HistoryGallery, { t, key: 'history' }),
|
|
637
578
|
],
|
|
638
579
|
})
|
|
639
580
|
}
|
|
@@ -680,6 +621,53 @@ window.__ModuleLoader__.load({
|
|
|
680
621
|
'settings.saving': 'Saving…',
|
|
681
622
|
'settings.saveFailed': 'Save failed',
|
|
682
623
|
'f.inherit': 'Inherit (default)',
|
|
624
|
+
'f.enabled': 'Enabled',
|
|
625
|
+
'f.enabledHint': 'Master switch: enable or disable the generate_image tool.',
|
|
626
|
+
'f.needFal': 'requires FAL API key',
|
|
627
|
+
'f.needCustom': 'requires custom endpoint URL',
|
|
628
|
+
'f.needCodex': 'uses Codex subscription session',
|
|
629
|
+
'f.needGrok': 'uses Grok subscription session',
|
|
630
|
+
'f.needLocal': 'connects to local ComfyUI/A1111',
|
|
631
|
+
'f.needSeedream': 'requires SeaDream API key',
|
|
632
|
+
'f.needGemini': 'requires Gemini API key',
|
|
633
|
+
'f.needReplicate': 'requires Replicate API token',
|
|
634
|
+
'f.replicateModel': 'Model (Replicate)',
|
|
635
|
+
'f.replicateModelHint': 'Replicate model identifier, e.g. black-forest-labs/flux-schnell.',
|
|
636
|
+
'f.replicateKeyEnv': 'API key reference (Replicate)',
|
|
637
|
+
'f.replicateKeyEnvHint': 'Credential reference / env var holding the Replicate API token.',
|
|
638
|
+
'p.replicateModel': 'Example: black-forest-labs/flux-schnell',
|
|
639
|
+
'p.replicateKeyEnv': 'Example: REPLICATE_API_TOKEN',
|
|
640
|
+
'f.seedreamModel': 'Model (SeaDream)',
|
|
641
|
+
'f.seedreamModelHint': 'SeaDream model identifier.',
|
|
642
|
+
'f.seedreamKeyEnv': 'API key reference (SeaDream)',
|
|
643
|
+
'f.seedreamKeyEnvHint': 'Credential reference / env var holding the SeaDream API key.',
|
|
644
|
+
'f.seedreamBaseURL': 'Base URL (SeaDream)',
|
|
645
|
+
'f.seedreamBaseURLHint': 'SeaDream endpoint URL (Volcengine Ark API).',
|
|
646
|
+
'p.seedreamModel': 'Example: Doubao-SeaDream-3.0',
|
|
647
|
+
'p.seedreamKeyEnv': 'Example: SEADREAM_API_KEY',
|
|
648
|
+
'p.seedreamBaseURL': 'Example: https://ark.cn-beijing.volces.com/api/v3',
|
|
649
|
+
'f.geminiModel': 'Model (Gemini)',
|
|
650
|
+
'f.geminiModelHint': 'Gemini Imagen model identifier.',
|
|
651
|
+
'f.geminiKeyEnv': 'API key reference (Gemini)',
|
|
652
|
+
'f.geminiKeyEnvHint': 'Credential reference / env var holding the Gemini API key.',
|
|
653
|
+
'p.geminiModel': 'Example: imagen-3.0-generate-002',
|
|
654
|
+
'p.geminiKeyEnv': 'Example: GEMINI_API_KEY',
|
|
655
|
+
'f.localComfyUrl': 'ComfyUI URL',
|
|
656
|
+
'f.localComfyUrlHint': 'Base URL of local ComfyUI server.',
|
|
657
|
+
'p.localComfyUrl': 'Example: http://127.0.0.1:8188',
|
|
658
|
+
'f.localA1111Url': 'Automatic1111 URL',
|
|
659
|
+
'f.localA1111UrlHint': 'Base URL of local Automatic1111 / SD WebUI.',
|
|
660
|
+
'p.localA1111Url': 'Example: http://127.0.0.1:7860',
|
|
661
|
+
'f.stylePreset': 'Default style preset',
|
|
662
|
+
'f.stylePresetHint': 'Style preset automatically applied if the tool call does not specify one.',
|
|
663
|
+
'f.enableLlmEnhancer': 'LLM prompt enhancer',
|
|
664
|
+
'f.enableLlmEnhancerHint': 'Automatically refine and expand prompts before generation using session LLM.',
|
|
665
|
+
'f.cacheTtlDays': 'Cache retention (days)',
|
|
666
|
+
'f.cacheTtlDaysHint': 'How many days downloaded images are stored in cache before cleanup.',
|
|
667
|
+
'p.cacheTtlDays': 'Example: 7',
|
|
668
|
+
'f.maxHistoryEntries': 'Max history entries',
|
|
669
|
+
'f.maxHistoryEntriesHint': 'Maximum number of items shown in generation history gallery.',
|
|
670
|
+
'p.maxHistoryEntries': 'Example: 50',
|
|
683
671
|
'f.provider': 'Provider',
|
|
684
672
|
'f.providerHint': 'fal — the FAL queue configured below. custom — any OpenAI-compatible API, configured further down. '
|
|
685
673
|
+ 'codex and grok — through a subscription you are logged into, no key needed here.',
|
|
@@ -743,6 +731,53 @@ window.__ModuleLoader__.load({
|
|
|
743
731
|
'settings.saving': 'Сохранение…',
|
|
744
732
|
'settings.saveFailed': 'Не удалось сохранить',
|
|
745
733
|
'f.inherit': 'Унаследовать (по умолчанию)',
|
|
734
|
+
'f.enabled': 'Включено',
|
|
735
|
+
'f.enabledHint': 'Главный переключатель: включить или отключить инструмент generate_image.',
|
|
736
|
+
'f.needFal': 'требуется ключ API FAL',
|
|
737
|
+
'f.needCustom': 'требуется базовый URL API',
|
|
738
|
+
'f.needCodex': 'использует сессию подписки Codex',
|
|
739
|
+
'f.needGrok': 'использует сессию подписки Grok',
|
|
740
|
+
'f.needLocal': 'подключается к локальному ComfyUI/A1111',
|
|
741
|
+
'f.needSeedream': 'требуется ключ API SeaDream',
|
|
742
|
+
'f.needGemini': 'требуется ключ API Gemini',
|
|
743
|
+
'f.needReplicate': 'требуется токен API Replicate',
|
|
744
|
+
'f.replicateModel': 'Модель (Replicate)',
|
|
745
|
+
'f.replicateModelHint': 'Идентификатор модели Replicate, например black-forest-labs/flux-schnell.',
|
|
746
|
+
'f.replicateKeyEnv': 'Ссылка на ключ (Replicate)',
|
|
747
|
+
'f.replicateKeyEnvHint': 'Имя credential-ссылки / переменной окружения с токеном Replicate.',
|
|
748
|
+
'p.replicateModel': 'Пример: black-forest-labs/flux-schnell',
|
|
749
|
+
'p.replicateKeyEnv': 'Пример: REPLICATE_API_TOKEN',
|
|
750
|
+
'f.seedreamModel': 'Модель (SeaDream)',
|
|
751
|
+
'f.seedreamModelHint': 'Идентификатор модели SeaDream.',
|
|
752
|
+
'f.seedreamKeyEnv': 'Ссылка на ключ (SeaDream)',
|
|
753
|
+
'f.seedreamKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом SeaDream.',
|
|
754
|
+
'f.seedreamBaseURL': 'Базовый URL (SeaDream)',
|
|
755
|
+
'f.seedreamBaseURLHint': 'Базовый URL API SeaDream (Volcengine Ark).',
|
|
756
|
+
'p.seedreamModel': 'Пример: Doubao-SeaDream-3.0',
|
|
757
|
+
'p.seedreamKeyEnv': 'Пример: SEADREAM_API_KEY',
|
|
758
|
+
'p.seedreamBaseURL': 'Пример: https://ark.cn-beijing.volces.com/api/v3',
|
|
759
|
+
'f.geminiModel': 'Модель (Gemini)',
|
|
760
|
+
'f.geminiModelHint': 'Идентификатор модели Gemini Imagen.',
|
|
761
|
+
'f.geminiKeyEnv': 'Ссылка на ключ (Gemini)',
|
|
762
|
+
'f.geminiKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом Gemini.',
|
|
763
|
+
'p.geminiModel': 'Пример: imagen-3.0-generate-002',
|
|
764
|
+
'p.geminiKeyEnv': 'Пример: GEMINI_API_KEY',
|
|
765
|
+
'f.localComfyUrl': 'URL ComfyUI',
|
|
766
|
+
'f.localComfyUrlHint': 'Базовый URL локального сервера ComfyUI.',
|
|
767
|
+
'p.localComfyUrl': 'Пример: http://127.0.0.1:8188',
|
|
768
|
+
'f.localA1111Url': 'URL Automatic1111',
|
|
769
|
+
'f.localA1111UrlHint': 'Базовый URL локального SD WebUI / Automatic1111.',
|
|
770
|
+
'p.localA1111Url': 'Пример: http://127.0.0.1:7860',
|
|
771
|
+
'f.stylePreset': 'Стилевой пресет по умолчанию',
|
|
772
|
+
'f.stylePresetHint': 'Пресет стилизации, применяемый если инструмент вызван без указания стиля.',
|
|
773
|
+
'f.enableLlmEnhancer': 'Улучшение промпта через LLM',
|
|
774
|
+
'f.enableLlmEnhancerHint': 'Автоматически детализировать и улучшать промпты через чат-модель перед генерацией.',
|
|
775
|
+
'f.cacheTtlDays': 'Срок хранения кэша (дней)',
|
|
776
|
+
'f.cacheTtlDaysHint': 'Сколько дней скачанные картинки хранятся в кэше до очистки.',
|
|
777
|
+
'p.cacheTtlDays': 'Пример: 7',
|
|
778
|
+
'f.maxHistoryEntries': 'Максимум записей в истории',
|
|
779
|
+
'f.maxHistoryEntriesHint': 'Максимальное количество картинок в галерее истории генераций.',
|
|
780
|
+
'p.maxHistoryEntries': 'Пример: 50',
|
|
746
781
|
'f.provider': 'Провайдер',
|
|
747
782
|
'f.providerHint': 'fal — очередь FAL, настроенная ниже. custom — любой OpenAI-совместимый API, настраивается дальше. '
|
|
748
783
|
+ 'codex и grok — через подписку, в которую вы вошли; ключ здесь не нужен.',
|
|
@@ -1226,36 +1261,40 @@ function FalImageCard(props) {
|
|
|
1226
1261
|
// Запасной путь: слот объявлен пакетом настроек ядра. Если в сборке его
|
|
1227
1262
|
// нет, регистрация бросает — тогда остаётся прежний раздел в боковом
|
|
1228
1263
|
// списке, чтобы настройки не пропали.
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
}
|
|
1244
|
-
if (!placedInPluginsTab) {
|
|
1245
|
-
ctx.slots.inject('settings.section', () =>
|
|
1246
|
-
ctx.slots.register(
|
|
1247
|
-
{
|
|
1248
|
-
name: 'settings.section',
|
|
1249
|
-
id: '@goodandready/dsh-image-gen',
|
|
1250
|
-
order: 34,
|
|
1251
|
-
locale: NS,
|
|
1252
|
-
label: () => t('settings.navLabel'),
|
|
1253
|
-
inject: () => cardOnce().inject(),
|
|
1254
|
-
},
|
|
1255
|
-
FalSettingsCard,
|
|
1256
|
-
),
|
|
1257
|
-
)
|
|
1264
|
+
function registerFirst(ctx, candidates, component) {
|
|
1265
|
+
if (!ctx || !ctx.slots) return undefined
|
|
1266
|
+
for (const entry of candidates) {
|
|
1267
|
+
try {
|
|
1268
|
+
if (typeof ctx.slots?.has === 'function' && !ctx.slots.has(entry.name)) {
|
|
1269
|
+
continue
|
|
1270
|
+
}
|
|
1271
|
+
let ok = false
|
|
1272
|
+
ctx.slots.inject(entry.name, () => {
|
|
1273
|
+
ctx.slots.register(entry, component)
|
|
1274
|
+
ok = true
|
|
1275
|
+
})
|
|
1276
|
+
if (ok) return entry.name
|
|
1277
|
+
} catch {}
|
|
1258
1278
|
}
|
|
1279
|
+
return undefined
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
registerFirst(ctx, [
|
|
1283
|
+
{
|
|
1284
|
+
name: 'settings.plugin.item',
|
|
1285
|
+
key: SETTINGS_NS,
|
|
1286
|
+
locale: NS,
|
|
1287
|
+
inject: () => cardOnce().inject(),
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
name: 'settings.section',
|
|
1291
|
+
id: '@goodandready/dsh-image-gen',
|
|
1292
|
+
order: 34,
|
|
1293
|
+
locale: NS,
|
|
1294
|
+
label: () => t('settings.navLabel'),
|
|
1295
|
+
inject: () => cardOnce().inject(),
|
|
1296
|
+
},
|
|
1297
|
+
], FalSettingsCard)
|
|
1259
1298
|
}
|
|
1260
1299
|
exports.apply = apply
|
|
1261
1300
|
exports.inject = inject
|
package/lib/index.js
CHANGED
|
@@ -1070,14 +1070,25 @@ export function apply(ctx, config) {
|
|
|
1070
1070
|
const filePath = path.join(outDir, name)
|
|
1071
1071
|
await writeFile(filePath, res.bytes)
|
|
1072
1072
|
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1073
|
-
|
|
1073
|
+
const summary = buildDualOutputMarkdown({
|
|
1074
|
+
action: 'background removed',
|
|
1075
|
+
filePath,
|
|
1076
|
+
width: res.width || attachment.width,
|
|
1077
|
+
height: res.height || attachment.height,
|
|
1078
|
+
mediaType: 'image/png',
|
|
1079
|
+
provider: 'fal',
|
|
1080
|
+
model: args.model || 'fal-ai/birefnet',
|
|
1081
|
+
attachmentId: attachment.attachmentId,
|
|
1082
|
+
})
|
|
1083
|
+
return toLosslessJson({
|
|
1084
|
+
summary,
|
|
1074
1085
|
path: filePath,
|
|
1075
1086
|
url: localUrl,
|
|
1076
1087
|
width: res.width || attachment.width,
|
|
1077
1088
|
height: res.height || attachment.height,
|
|
1078
1089
|
format: 'png',
|
|
1079
1090
|
attachment,
|
|
1080
|
-
}
|
|
1091
|
+
})
|
|
1081
1092
|
},
|
|
1082
1093
|
}),
|
|
1083
1094
|
)
|
|
@@ -1128,14 +1139,25 @@ export function apply(ctx, config) {
|
|
|
1128
1139
|
const filePath = path.join(outDir, name)
|
|
1129
1140
|
await writeFile(filePath, res.bytes)
|
|
1130
1141
|
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1131
|
-
|
|
1142
|
+
const summary = buildDualOutputMarkdown({
|
|
1143
|
+
action: `upscaled (${args.scale || 2}x)`,
|
|
1144
|
+
filePath,
|
|
1145
|
+
width: res.width || attachment.width,
|
|
1146
|
+
height: res.height || attachment.height,
|
|
1147
|
+
mediaType: 'image/png',
|
|
1148
|
+
provider: 'fal',
|
|
1149
|
+
model: 'fal-ai/clarity-upscaler',
|
|
1150
|
+
attachmentId: attachment.attachmentId,
|
|
1151
|
+
})
|
|
1152
|
+
return toLosslessJson({
|
|
1153
|
+
summary,
|
|
1132
1154
|
path: filePath,
|
|
1133
1155
|
url: localUrl,
|
|
1134
1156
|
scale: args.scale || 2,
|
|
1135
1157
|
width: res.width || attachment.width,
|
|
1136
1158
|
height: res.height || attachment.height,
|
|
1137
1159
|
attachment,
|
|
1138
|
-
}
|
|
1160
|
+
})
|
|
1139
1161
|
},
|
|
1140
1162
|
}),
|
|
1141
1163
|
)
|
|
@@ -1173,11 +1195,13 @@ export function apply(ctx, config) {
|
|
|
1173
1195
|
await mkdir(outDir, { recursive: true })
|
|
1174
1196
|
const filePath = path.join(outDir, name)
|
|
1175
1197
|
await writeFile(filePath, res.bytes)
|
|
1176
|
-
|
|
1198
|
+
const summary = `### Vectorized SVG Export\n- **File**: ${filePath}\n- **Format**: SVG (XML vector)\n- **Color mode**: ${args.color_mode || 'color'}\n- **Palette size**: ${res.palette ? res.palette.length : 16} colors\n`
|
|
1199
|
+
return toLosslessJson({
|
|
1200
|
+
summary,
|
|
1177
1201
|
path: filePath,
|
|
1178
1202
|
svg: res.svg,
|
|
1179
1203
|
format: 'svg',
|
|
1180
|
-
}
|
|
1204
|
+
})
|
|
1181
1205
|
},
|
|
1182
1206
|
}),
|
|
1183
1207
|
)
|
|
@@ -1232,13 +1256,24 @@ export function apply(ctx, config) {
|
|
|
1232
1256
|
const filePath = path.join(outDir, name)
|
|
1233
1257
|
await writeFile(filePath, res.bytes)
|
|
1234
1258
|
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1235
|
-
|
|
1259
|
+
const summary = buildDualOutputMarkdown({
|
|
1260
|
+
action: 'blended composition',
|
|
1261
|
+
filePath,
|
|
1262
|
+
width: res.width || attachment.width,
|
|
1263
|
+
height: res.height || attachment.height,
|
|
1264
|
+
mediaType: 'image/png',
|
|
1265
|
+
provider: 'fal',
|
|
1266
|
+
model: 'fal-ai/image-blend',
|
|
1267
|
+
attachmentId: attachment.attachmentId,
|
|
1268
|
+
})
|
|
1269
|
+
return toLosslessJson({
|
|
1270
|
+
summary,
|
|
1236
1271
|
path: filePath,
|
|
1237
1272
|
url: localUrl,
|
|
1238
1273
|
width: res.width || attachment.width,
|
|
1239
1274
|
height: res.height || attachment.height,
|
|
1240
1275
|
attachment,
|
|
1241
|
-
}
|
|
1276
|
+
})
|
|
1242
1277
|
},
|
|
1243
1278
|
}),
|
|
1244
1279
|
)
|
|
@@ -1610,12 +1645,13 @@ export function apply(ctx, config) {
|
|
|
1610
1645
|
if (!colors || colors.length === 0) {
|
|
1611
1646
|
if (args.image) {
|
|
1612
1647
|
const source = await resolveSource(ctx, exec, args.image)
|
|
1613
|
-
if (source
|
|
1614
|
-
|
|
1648
|
+
if (!source || !source.bytes) {
|
|
1649
|
+
throw new Error('Image not found or unreadable: ' + args.image)
|
|
1615
1650
|
}
|
|
1651
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1616
1652
|
}
|
|
1617
1653
|
}
|
|
1618
|
-
return extractDesignTokens(colors, { prefix: args.prefix })
|
|
1654
|
+
return toLosslessJson(extractDesignTokens(colors, { prefix: args.prefix }))
|
|
1619
1655
|
},
|
|
1620
1656
|
}),
|
|
1621
1657
|
)
|
|
@@ -1648,12 +1684,13 @@ export function apply(ctx, config) {
|
|
|
1648
1684
|
if (!colors || colors.length === 0) {
|
|
1649
1685
|
if (args.image) {
|
|
1650
1686
|
const source = await resolveSource(ctx, exec, args.image)
|
|
1651
|
-
if (source
|
|
1652
|
-
|
|
1687
|
+
if (!source || !source.bytes) {
|
|
1688
|
+
throw new Error('Image not found or unreadable: ' + args.image)
|
|
1653
1689
|
}
|
|
1690
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1654
1691
|
}
|
|
1655
1692
|
}
|
|
1656
|
-
return generateCssGradient(colors, args.type || 'mesh')
|
|
1693
|
+
return toLosslessJson(generateCssGradient(colors, args.type || 'mesh'))
|
|
1657
1694
|
},
|
|
1658
1695
|
}),
|
|
1659
1696
|
)
|
|
@@ -1689,12 +1726,13 @@ export function apply(ctx, config) {
|
|
|
1689
1726
|
if (!colors || colors.length === 0) {
|
|
1690
1727
|
if (args.image) {
|
|
1691
1728
|
const source = await resolveSource(ctx, exec, args.image)
|
|
1692
|
-
if (source
|
|
1693
|
-
|
|
1729
|
+
if (!source || !source.bytes) {
|
|
1730
|
+
throw new Error('Image not found or unreadable: ' + args.image)
|
|
1694
1731
|
}
|
|
1732
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1695
1733
|
}
|
|
1696
1734
|
}
|
|
1697
|
-
return checkWcagContrast(colors, args.text_color || '#ffffff')
|
|
1735
|
+
return toLosslessJson(checkWcagContrast(colors, args.text_color || '#ffffff'))
|
|
1698
1736
|
},
|
|
1699
1737
|
}),
|
|
1700
1738
|
)
|
|
@@ -1769,7 +1807,7 @@ export function apply(ctx, config) {
|
|
|
1769
1807
|
},
|
|
1770
1808
|
},
|
|
1771
1809
|
async execute(args, exec) {
|
|
1772
|
-
|
|
1810
|
+
const res = await generatePwaIconSuite({
|
|
1773
1811
|
name: args.name || 'App',
|
|
1774
1812
|
shortName: args.short_name || args.name || 'App',
|
|
1775
1813
|
themeColor: args.theme_color || '#0f172a',
|
|
@@ -1777,6 +1815,7 @@ export function apply(ctx, config) {
|
|
|
1777
1815
|
iconsDir: args.icons_dir || 'icons',
|
|
1778
1816
|
sourcePath: args.image || null,
|
|
1779
1817
|
})
|
|
1818
|
+
return toLosslessJson(res)
|
|
1780
1819
|
},
|
|
1781
1820
|
}),
|
|
1782
1821
|
)
|
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.10",
|
|
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",
|