@goodandready/dsh-image-gen 0.10.6 → 0.10.8
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 +6 -0
- package/lib/index.js +70 -26
- package/lib/providers.js +119 -5
- 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">
|
|
@@ -70,6 +76,13 @@
|
|
|
70
76
|
- **React Cleanup (#139)**: removed dead state hooks from `FalImageCard`.
|
|
71
77
|
- **Complete Localization (#140)**: eliminated hardcoded strings, wiring comprehensive dictionaries for en, ru, and zh.
|
|
72
78
|
|
|
79
|
+
|
|
80
|
+
### 🚀 What's New in v0.10.7 (#197)
|
|
81
|
+
* **Robust Error Formatting**: completely prevents `[object Object]` from appearing in provider refusal chains, extracting deep `.message`, `.detail`, and `.error` objects cleanly.
|
|
82
|
+
* **Deduplicated Provider Prefixes**: eliminates redundant `codex: codex: ...` prefixes.
|
|
83
|
+
* **FAL Credential Aliasing**: seamless fallback between `FAL_API_KEY` and `FAL_KEY` in credentials and environment.
|
|
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`.
|
|
85
|
+
|
|
73
86
|
## 🎨 Supported Generation Backends
|
|
74
87
|
|
|
75
88
|
* **`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-улучшителя.
|
package/lib/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
pixelDiff,
|
|
34
34
|
normalizeCount,
|
|
35
35
|
tryGenerate,
|
|
36
|
+
resolveApiKeyCandidates,
|
|
36
37
|
fallbackOrder,
|
|
37
38
|
normalizeMediaType,
|
|
38
39
|
embedPngMetadata,
|
|
@@ -354,14 +355,18 @@ export function slugify(input) {
|
|
|
354
355
|
/** Resolve an API key for one call (credentials service first, then env). */
|
|
355
356
|
export async function resolveApiKey(ctx, ref) {
|
|
356
357
|
if (!ref) return ''
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
358
|
+
const candidates = resolveApiKeyCandidates(ref)
|
|
359
|
+
|
|
360
|
+
for (const candidate of candidates) {
|
|
361
|
+
try {
|
|
362
|
+
const resolved = await ctx.credentials.resolve(credentialRef(candidate))
|
|
363
|
+
if (resolved && resolved.value) return resolved.value
|
|
364
|
+
} catch {
|
|
365
|
+
// fall through to the environment
|
|
366
|
+
}
|
|
367
|
+
const fromEnv = process.env[candidate]
|
|
368
|
+
if (fromEnv) return fromEnv
|
|
362
369
|
}
|
|
363
|
-
const fromEnv = process.env[ref]
|
|
364
|
-
if (fromEnv) return fromEnv
|
|
365
370
|
throw new Error(
|
|
366
371
|
`API key not configured: set credential/env "${ref}" (Web: Settings → Credentials, or add "${ref}: <key>" to $DSH_HOME/.credentials.yaml)`,
|
|
367
372
|
)
|
|
@@ -813,7 +818,7 @@ export function apply(ctx, config) {
|
|
|
813
818
|
const effectiveGuidance = styleInfo.guidanceScale
|
|
814
819
|
const providers = makeProviders(
|
|
815
820
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
816
|
-
{ 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 },
|
|
821
|
+
{ 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, aspectRatio: args.aspect_ratio },
|
|
817
822
|
)
|
|
818
823
|
// Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
|
|
819
824
|
// отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
|
|
@@ -1065,14 +1070,25 @@ export function apply(ctx, config) {
|
|
|
1065
1070
|
const filePath = path.join(outDir, name)
|
|
1066
1071
|
await writeFile(filePath, res.bytes)
|
|
1067
1072
|
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1068
|
-
|
|
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,
|
|
1069
1085
|
path: filePath,
|
|
1070
1086
|
url: localUrl,
|
|
1071
1087
|
width: res.width || attachment.width,
|
|
1072
1088
|
height: res.height || attachment.height,
|
|
1073
1089
|
format: 'png',
|
|
1074
1090
|
attachment,
|
|
1075
|
-
}
|
|
1091
|
+
})
|
|
1076
1092
|
},
|
|
1077
1093
|
}),
|
|
1078
1094
|
)
|
|
@@ -1123,14 +1139,25 @@ export function apply(ctx, config) {
|
|
|
1123
1139
|
const filePath = path.join(outDir, name)
|
|
1124
1140
|
await writeFile(filePath, res.bytes)
|
|
1125
1141
|
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1126
|
-
|
|
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,
|
|
1127
1154
|
path: filePath,
|
|
1128
1155
|
url: localUrl,
|
|
1129
1156
|
scale: args.scale || 2,
|
|
1130
1157
|
width: res.width || attachment.width,
|
|
1131
1158
|
height: res.height || attachment.height,
|
|
1132
1159
|
attachment,
|
|
1133
|
-
}
|
|
1160
|
+
})
|
|
1134
1161
|
},
|
|
1135
1162
|
}),
|
|
1136
1163
|
)
|
|
@@ -1168,11 +1195,13 @@ export function apply(ctx, config) {
|
|
|
1168
1195
|
await mkdir(outDir, { recursive: true })
|
|
1169
1196
|
const filePath = path.join(outDir, name)
|
|
1170
1197
|
await writeFile(filePath, res.bytes)
|
|
1171
|
-
|
|
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,
|
|
1172
1201
|
path: filePath,
|
|
1173
1202
|
svg: res.svg,
|
|
1174
1203
|
format: 'svg',
|
|
1175
|
-
}
|
|
1204
|
+
})
|
|
1176
1205
|
},
|
|
1177
1206
|
}),
|
|
1178
1207
|
)
|
|
@@ -1227,13 +1256,24 @@ export function apply(ctx, config) {
|
|
|
1227
1256
|
const filePath = path.join(outDir, name)
|
|
1228
1257
|
await writeFile(filePath, res.bytes)
|
|
1229
1258
|
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1230
|
-
|
|
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,
|
|
1231
1271
|
path: filePath,
|
|
1232
1272
|
url: localUrl,
|
|
1233
1273
|
width: res.width || attachment.width,
|
|
1234
1274
|
height: res.height || attachment.height,
|
|
1235
1275
|
attachment,
|
|
1236
|
-
}
|
|
1276
|
+
})
|
|
1237
1277
|
},
|
|
1238
1278
|
}),
|
|
1239
1279
|
)
|
|
@@ -1605,12 +1645,13 @@ export function apply(ctx, config) {
|
|
|
1605
1645
|
if (!colors || colors.length === 0) {
|
|
1606
1646
|
if (args.image) {
|
|
1607
1647
|
const source = await resolveSource(ctx, exec, args.image)
|
|
1608
|
-
if (source
|
|
1609
|
-
|
|
1648
|
+
if (!source || !source.bytes) {
|
|
1649
|
+
throw new Error('Image not found or unreadable: ' + args.image)
|
|
1610
1650
|
}
|
|
1651
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1611
1652
|
}
|
|
1612
1653
|
}
|
|
1613
|
-
return extractDesignTokens(colors, { prefix: args.prefix })
|
|
1654
|
+
return toLosslessJson(extractDesignTokens(colors, { prefix: args.prefix }))
|
|
1614
1655
|
},
|
|
1615
1656
|
}),
|
|
1616
1657
|
)
|
|
@@ -1643,12 +1684,13 @@ export function apply(ctx, config) {
|
|
|
1643
1684
|
if (!colors || colors.length === 0) {
|
|
1644
1685
|
if (args.image) {
|
|
1645
1686
|
const source = await resolveSource(ctx, exec, args.image)
|
|
1646
|
-
if (source
|
|
1647
|
-
|
|
1687
|
+
if (!source || !source.bytes) {
|
|
1688
|
+
throw new Error('Image not found or unreadable: ' + args.image)
|
|
1648
1689
|
}
|
|
1690
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1649
1691
|
}
|
|
1650
1692
|
}
|
|
1651
|
-
return generateCssGradient(colors, args.type || 'mesh')
|
|
1693
|
+
return toLosslessJson(generateCssGradient(colors, args.type || 'mesh'))
|
|
1652
1694
|
},
|
|
1653
1695
|
}),
|
|
1654
1696
|
)
|
|
@@ -1684,12 +1726,13 @@ export function apply(ctx, config) {
|
|
|
1684
1726
|
if (!colors || colors.length === 0) {
|
|
1685
1727
|
if (args.image) {
|
|
1686
1728
|
const source = await resolveSource(ctx, exec, args.image)
|
|
1687
|
-
if (source
|
|
1688
|
-
|
|
1729
|
+
if (!source || !source.bytes) {
|
|
1730
|
+
throw new Error('Image not found or unreadable: ' + args.image)
|
|
1689
1731
|
}
|
|
1732
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1690
1733
|
}
|
|
1691
1734
|
}
|
|
1692
|
-
return checkWcagContrast(colors, args.text_color || '#ffffff')
|
|
1735
|
+
return toLosslessJson(checkWcagContrast(colors, args.text_color || '#ffffff'))
|
|
1693
1736
|
},
|
|
1694
1737
|
}),
|
|
1695
1738
|
)
|
|
@@ -1764,7 +1807,7 @@ export function apply(ctx, config) {
|
|
|
1764
1807
|
},
|
|
1765
1808
|
},
|
|
1766
1809
|
async execute(args, exec) {
|
|
1767
|
-
|
|
1810
|
+
const res = await generatePwaIconSuite({
|
|
1768
1811
|
name: args.name || 'App',
|
|
1769
1812
|
shortName: args.short_name || args.name || 'App',
|
|
1770
1813
|
themeColor: args.theme_color || '#0f172a',
|
|
@@ -1772,6 +1815,7 @@ export function apply(ctx, config) {
|
|
|
1772
1815
|
iconsDir: args.icons_dir || 'icons',
|
|
1773
1816
|
sourcePath: args.image || null,
|
|
1774
1817
|
})
|
|
1818
|
+
return toLosslessJson(res)
|
|
1775
1819
|
},
|
|
1776
1820
|
}),
|
|
1777
1821
|
)
|
package/lib/providers.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Возвращает список кандидатов для имени ключа API с учётом известных алиасов (напр. FAL_API_KEY <-> FAL_KEY).
|
|
3
|
+
*/
|
|
4
|
+
export function resolveApiKeyCandidates(ref) {
|
|
5
|
+
if (!ref) return []
|
|
6
|
+
const candidates = [ref]
|
|
7
|
+
if (ref === 'FAL_API_KEY') candidates.push('FAL_KEY')
|
|
8
|
+
else if (ref === 'FAL_KEY') candidates.push('FAL_API_KEY')
|
|
9
|
+
return candidates
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
|
|
2
13
|
export const PROVIDER_MAX_COUNTS = {
|
|
3
14
|
fal: 4,
|
|
@@ -145,6 +156,81 @@ export function isFatalClientError(error) {
|
|
|
145
156
|
)
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Safely extract a human-readable message from any error, object, or response.
|
|
161
|
+
* Completely eliminates "[object Object]" and prevents duplicate provider prefixes (e.g. "codex: codex: ...").
|
|
162
|
+
*/
|
|
163
|
+
export function formatErrorMessage(e, providerKey = '') {
|
|
164
|
+
if (e === null || e === undefined) {
|
|
165
|
+
return providerKey ? `${providerKey}: unknown error` : 'unknown error'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let msg = ''
|
|
169
|
+
if (typeof e === 'string') {
|
|
170
|
+
msg = e
|
|
171
|
+
} else if (typeof e === 'object') {
|
|
172
|
+
if (typeof e.message === 'string' && e.message && e.message !== '[object Object]') {
|
|
173
|
+
msg = e.message
|
|
174
|
+
} else if (typeof e.detail === 'string' && e.detail) {
|
|
175
|
+
msg = e.detail
|
|
176
|
+
} else if (e.error) {
|
|
177
|
+
if (typeof e.error === 'string') msg = e.error
|
|
178
|
+
else if (typeof e.error.message === 'string') msg = e.error.message
|
|
179
|
+
else if (typeof e.error.detail === 'string') msg = e.error.detail
|
|
180
|
+
else {
|
|
181
|
+
try { msg = JSON.stringify(e.error) } catch {}
|
|
182
|
+
}
|
|
183
|
+
} else if (typeof e.statusText === 'string' && e.statusText) {
|
|
184
|
+
msg = `HTTP ${e.status || ''} ${e.statusText}`.trim()
|
|
185
|
+
} else if (typeof e.cause === 'string') {
|
|
186
|
+
msg = e.cause
|
|
187
|
+
} else if (e.cause && typeof e.cause.message === 'string') {
|
|
188
|
+
msg = e.cause.message
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!msg || msg === '[object Object]') {
|
|
192
|
+
try {
|
|
193
|
+
const json = JSON.stringify(e)
|
|
194
|
+
if (json && json !== '{}') {
|
|
195
|
+
msg = json.slice(0, 500)
|
|
196
|
+
}
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!msg || msg === '[object Object]') {
|
|
201
|
+
const keys = Object.getOwnPropertyNames(e)
|
|
202
|
+
if (keys.length) {
|
|
203
|
+
msg = `error [${keys.join(', ')}]`
|
|
204
|
+
} else {
|
|
205
|
+
msg = String(e)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
msg = String(e)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
msg = String(msg || 'unknown error').trim()
|
|
213
|
+
|
|
214
|
+
if (providerKey) {
|
|
215
|
+
const prefixRegex = new RegExp(`^${providerKey}\\s*:\\s*`, 'i')
|
|
216
|
+
while (prefixRegex.test(msg)) {
|
|
217
|
+
msg = msg.replace(prefixRegex, '').trim()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!msg || msg === '[object Object]') {
|
|
222
|
+
msg = 'unknown error object'
|
|
223
|
+
} else if (msg.includes('[object Object]')) {
|
|
224
|
+
msg = msg.replace(/\[object Object\]/g, 'unknown error object').trim()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (providerKey) {
|
|
228
|
+
return `${providerKey}: ${msg}`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return msg
|
|
232
|
+
}
|
|
233
|
+
|
|
148
234
|
export async function tryGenerate(generators, order, seed, promptArg) {
|
|
149
235
|
const refusals = []
|
|
150
236
|
for (const key of order) {
|
|
@@ -153,9 +239,9 @@ export async function tryGenerate(generators, order, seed, promptArg) {
|
|
|
153
239
|
return produced
|
|
154
240
|
} catch (e) {
|
|
155
241
|
if (isFatalClientError(e)) {
|
|
156
|
-
throw new Error(`Client error on provider ${key} (not retrying fallback): ${e
|
|
242
|
+
throw new Error(`Client error on provider ${key} (not retrying fallback): ${formatErrorMessage(e, key)}`)
|
|
157
243
|
}
|
|
158
|
-
refusals.push(
|
|
244
|
+
refusals.push(formatErrorMessage(e, key))
|
|
159
245
|
}
|
|
160
246
|
}
|
|
161
247
|
throw new Error(`Image generation failed on all providers. ${refusals.join('; ')}`)
|
|
@@ -169,6 +255,34 @@ export function normalizeCount(count, max = 4) {
|
|
|
169
255
|
|
|
170
256
|
// Размеры, которыми оперируют подписки, — свой набор, не похожий на именованные
|
|
171
257
|
// размеры FAL. Перевод один к одному по смыслу: квадрат, вертикаль, горизонталь.
|
|
258
|
+
/**
|
|
259
|
+
* Разрешает размер для подписочных сервисов (codex / grok).
|
|
260
|
+
* Понимает именованные размеры (landscape_16_9 и др.), пропорции (16:9, 3:2, 9:16 и др.)
|
|
261
|
+
* и размеры в пикселях [width, height], исключая дефолтный квадрат при запросе соотношений сторон.
|
|
262
|
+
*/
|
|
263
|
+
export function resolveSubscriptionSize(size, aspectPixels, aspectRatio) {
|
|
264
|
+
if (size && SUBSCRIPTION_SIZES[size]) {
|
|
265
|
+
return SUBSCRIPTION_SIZES[size]
|
|
266
|
+
}
|
|
267
|
+
const ratio = String(aspectRatio || '').trim()
|
|
268
|
+
if (ratio === '16:9' || ratio === '3:2' || ratio === '4:3') {
|
|
269
|
+
return '1536x1024'
|
|
270
|
+
}
|
|
271
|
+
if (ratio === '9:16' || ratio === '2:3' || ratio === '3:4') {
|
|
272
|
+
return '1024x1536'
|
|
273
|
+
}
|
|
274
|
+
if (ratio === '1:1') {
|
|
275
|
+
return '1024x1024'
|
|
276
|
+
}
|
|
277
|
+
if (Array.isArray(aspectPixels) && aspectPixels.length === 2) {
|
|
278
|
+
const [w, h] = aspectPixels
|
|
279
|
+
if (w > h) return '1536x1024'
|
|
280
|
+
if (h > w) return '1024x1536'
|
|
281
|
+
return '1024x1024'
|
|
282
|
+
}
|
|
283
|
+
return '1024x1024'
|
|
284
|
+
}
|
|
285
|
+
|
|
172
286
|
export const SUBSCRIPTION_SIZES = {
|
|
173
287
|
square_hd: '1024x1024',
|
|
174
288
|
square: '1024x1024',
|
|
@@ -349,7 +463,7 @@ export async function pixelDiff(a, b) {
|
|
|
349
463
|
|
|
350
464
|
export function makeProviders(deps, job) {
|
|
351
465
|
const { fetchImpl, resolveKey, cfg } = deps
|
|
352
|
-
const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels } = job
|
|
466
|
+
const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels, aspectRatio } = job
|
|
353
467
|
|
|
354
468
|
async function fal(seedArg = seed, promptArg = prompt) {
|
|
355
469
|
const key = await resolveKey(cfg.apiKeyEnv)
|
|
@@ -498,12 +612,12 @@ export function makeProviders(deps, job) {
|
|
|
498
612
|
produced = await images.generate({
|
|
499
613
|
provider,
|
|
500
614
|
prompt: promptArg,
|
|
501
|
-
size:
|
|
615
|
+
size: resolveSubscriptionSize(size, aspectPixels, aspectRatio),
|
|
502
616
|
quality: cfg.subscriptionQuality || undefined,
|
|
503
617
|
signal,
|
|
504
618
|
})
|
|
505
619
|
} catch (e) {
|
|
506
|
-
return { ok: false, provider, reason:
|
|
620
|
+
return { ok: false, provider, reason: formatErrorMessage(e, provider) }
|
|
507
621
|
}
|
|
508
622
|
const first = Array.isArray(produced) ? produced[0] : null
|
|
509
623
|
if (!first || !first.b64_json) {
|
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.8",
|
|
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",
|