@goodandready/dsh-image-gen 0.11.39 → 0.11.40
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/capabilities.js +433 -0
- package/lib/client.js +5 -0
- package/lib/config-schema.js +5 -0
- package/lib/index.js +45 -0
- package/lib/project-profiles.js +234 -0
- package/lib/register-tools.js +8 -0
- package/lib/tools/capabilities.js +139 -0
- package/lib/tools/project-profiles.js +139 -0
- package/package.json +1 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Registry and Model Selector (#434)
|
|
3
|
+
* Provides comprehensive capability mapping, strict pre-flight validation,
|
|
4
|
+
* cost estimation, and recommendation for task-specific models and providers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const CAPABILITY_REGISTRY = [
|
|
8
|
+
// Fal.ai models
|
|
9
|
+
{
|
|
10
|
+
provider: 'fal',
|
|
11
|
+
modelId: 'fal-ai/flux/dev',
|
|
12
|
+
displayName: 'FLUX.1 [dev]',
|
|
13
|
+
capabilities: ['txt2img', 'img2img', 'inpaint', 'high_res', 'text_rendering'],
|
|
14
|
+
maxResolution: 2048,
|
|
15
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4', '21:9'],
|
|
16
|
+
features: {
|
|
17
|
+
textRendering: true,
|
|
18
|
+
facePreservation: false,
|
|
19
|
+
maskInpainting: true,
|
|
20
|
+
referenceConditioning: false,
|
|
21
|
+
controlnet: false,
|
|
22
|
+
},
|
|
23
|
+
estimatedCostUSD: 0.025,
|
|
24
|
+
pricingSource: 'fal.ai/pricing (2026-09)',
|
|
25
|
+
verifiedDate: '2026-09-15',
|
|
26
|
+
tier: 'pro',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
provider: 'fal',
|
|
30
|
+
modelId: 'fal-ai/flux/schnell',
|
|
31
|
+
displayName: 'FLUX.1 [schnell]',
|
|
32
|
+
capabilities: ['txt2img', 'text_rendering'],
|
|
33
|
+
maxResolution: 1024,
|
|
34
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
35
|
+
features: {
|
|
36
|
+
textRendering: true,
|
|
37
|
+
facePreservation: false,
|
|
38
|
+
maskInpainting: false,
|
|
39
|
+
referenceConditioning: false,
|
|
40
|
+
controlnet: false,
|
|
41
|
+
},
|
|
42
|
+
estimatedCostUSD: 0.003,
|
|
43
|
+
pricingSource: 'fal.ai/pricing (2026-09)',
|
|
44
|
+
verifiedDate: '2026-09-15',
|
|
45
|
+
tier: 'economy',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
provider: 'fal',
|
|
49
|
+
modelId: 'fal-ai/flux-general/image-to-image',
|
|
50
|
+
displayName: 'FLUX.1 Image-to-Image',
|
|
51
|
+
capabilities: ['img2img', 'reference'],
|
|
52
|
+
maxResolution: 1536,
|
|
53
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
54
|
+
features: {
|
|
55
|
+
textRendering: true,
|
|
56
|
+
facePreservation: false,
|
|
57
|
+
maskInpainting: false,
|
|
58
|
+
referenceConditioning: true,
|
|
59
|
+
controlnet: false,
|
|
60
|
+
},
|
|
61
|
+
estimatedCostUSD: 0.03,
|
|
62
|
+
pricingSource: 'fal.ai/pricing (2026-09)',
|
|
63
|
+
verifiedDate: '2026-09-15',
|
|
64
|
+
tier: 'pro',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
provider: 'fal',
|
|
68
|
+
modelId: 'fal-ai/recraft-v3',
|
|
69
|
+
displayName: 'Recraft v3',
|
|
70
|
+
capabilities: ['txt2img', 'vector', 'text_rendering', 'high_res'],
|
|
71
|
+
maxResolution: 2048,
|
|
72
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
73
|
+
features: {
|
|
74
|
+
textRendering: true,
|
|
75
|
+
facePreservation: false,
|
|
76
|
+
maskInpainting: false,
|
|
77
|
+
referenceConditioning: false,
|
|
78
|
+
controlnet: false,
|
|
79
|
+
},
|
|
80
|
+
estimatedCostUSD: 0.04,
|
|
81
|
+
pricingSource: 'fal.ai/pricing (2026-09)',
|
|
82
|
+
verifiedDate: '2026-09-15',
|
|
83
|
+
tier: 'pro',
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Google Gemini Imagen models
|
|
87
|
+
{
|
|
88
|
+
provider: 'gemini',
|
|
89
|
+
modelId: 'imagen-3.0-generate-002',
|
|
90
|
+
displayName: 'Google Imagen 3.0',
|
|
91
|
+
capabilities: ['txt2img', 'text_rendering', 'high_res'],
|
|
92
|
+
maxResolution: 2048,
|
|
93
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
94
|
+
features: {
|
|
95
|
+
textRendering: true,
|
|
96
|
+
facePreservation: false,
|
|
97
|
+
maskInpainting: false,
|
|
98
|
+
referenceConditioning: false,
|
|
99
|
+
controlnet: false,
|
|
100
|
+
},
|
|
101
|
+
estimatedCostUSD: 0.03,
|
|
102
|
+
pricingSource: 'ai.google.dev (2026-09)',
|
|
103
|
+
verifiedDate: '2026-09-01',
|
|
104
|
+
tier: 'pro',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
provider: 'gemini',
|
|
108
|
+
modelId: 'imagen-3.0-capability-001',
|
|
109
|
+
displayName: 'Google Imagen 3.0 Editing',
|
|
110
|
+
capabilities: ['txt2img', 'img2img', 'inpaint', 'outpaint'],
|
|
111
|
+
maxResolution: 1536,
|
|
112
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
113
|
+
features: {
|
|
114
|
+
textRendering: true,
|
|
115
|
+
facePreservation: false,
|
|
116
|
+
maskInpainting: true,
|
|
117
|
+
referenceConditioning: true,
|
|
118
|
+
controlnet: false,
|
|
119
|
+
},
|
|
120
|
+
estimatedCostUSD: 0.035,
|
|
121
|
+
pricingSource: 'ai.google.dev (2026-09)',
|
|
122
|
+
verifiedDate: '2026-09-01',
|
|
123
|
+
tier: 'pro',
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Replicate models
|
|
127
|
+
{
|
|
128
|
+
provider: 'replicate',
|
|
129
|
+
modelId: 'black-forest-labs/flux-1.1-pro',
|
|
130
|
+
displayName: 'Replicate FLUX 1.1 Pro',
|
|
131
|
+
capabilities: ['txt2img', 'text_rendering', 'high_res'],
|
|
132
|
+
maxResolution: 2048,
|
|
133
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4', '21:9'],
|
|
134
|
+
features: {
|
|
135
|
+
textRendering: true,
|
|
136
|
+
facePreservation: false,
|
|
137
|
+
maskInpainting: false,
|
|
138
|
+
referenceConditioning: false,
|
|
139
|
+
controlnet: false,
|
|
140
|
+
},
|
|
141
|
+
estimatedCostUSD: 0.04,
|
|
142
|
+
pricingSource: 'replicate.com/pricing',
|
|
143
|
+
verifiedDate: '2026-09-10',
|
|
144
|
+
tier: 'pro',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
provider: 'replicate',
|
|
148
|
+
modelId: 'stability-ai/sdxl',
|
|
149
|
+
displayName: 'Replicate SDXL',
|
|
150
|
+
capabilities: ['txt2img', 'img2img', 'inpaint'],
|
|
151
|
+
maxResolution: 1024,
|
|
152
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
153
|
+
features: {
|
|
154
|
+
textRendering: false,
|
|
155
|
+
facePreservation: false,
|
|
156
|
+
maskInpainting: true,
|
|
157
|
+
referenceConditioning: true,
|
|
158
|
+
controlnet: false,
|
|
159
|
+
},
|
|
160
|
+
estimatedCostUSD: 0.018,
|
|
161
|
+
pricingSource: 'replicate.com/pricing',
|
|
162
|
+
verifiedDate: '2026-09-10',
|
|
163
|
+
tier: 'economy',
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Local (ComfyUI / Stable Diffusion / A1111)
|
|
167
|
+
{
|
|
168
|
+
provider: 'local',
|
|
169
|
+
modelId: 'sdxl_base',
|
|
170
|
+
displayName: 'Local ComfyUI SDXL Base',
|
|
171
|
+
capabilities: ['txt2img', 'img2img', 'inpaint', 'high_res', 'face_preservation', 'controlnet'],
|
|
172
|
+
maxResolution: 2048,
|
|
173
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4', '21:9'],
|
|
174
|
+
features: {
|
|
175
|
+
textRendering: false,
|
|
176
|
+
facePreservation: true,
|
|
177
|
+
maskInpainting: true,
|
|
178
|
+
referenceConditioning: true,
|
|
179
|
+
controlnet: true,
|
|
180
|
+
},
|
|
181
|
+
estimatedCostUSD: 0.0,
|
|
182
|
+
pricingSource: 'local-hardware (0 USD)',
|
|
183
|
+
verifiedDate: '2026-10-01',
|
|
184
|
+
tier: 'free',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
provider: 'local',
|
|
188
|
+
modelId: 'flux_local',
|
|
189
|
+
displayName: 'Local ComfyUI FLUX',
|
|
190
|
+
capabilities: ['txt2img', 'img2img', 'text_rendering', 'high_res'],
|
|
191
|
+
maxResolution: 2048,
|
|
192
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4', '21:9'],
|
|
193
|
+
features: {
|
|
194
|
+
textRendering: true,
|
|
195
|
+
facePreservation: false,
|
|
196
|
+
maskInpainting: false,
|
|
197
|
+
referenceConditioning: false,
|
|
198
|
+
controlnet: false,
|
|
199
|
+
},
|
|
200
|
+
estimatedCostUSD: 0.0,
|
|
201
|
+
pricingSource: 'local-hardware (0 USD)',
|
|
202
|
+
verifiedDate: '2026-10-01',
|
|
203
|
+
tier: 'free',
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Seedream backend
|
|
207
|
+
{
|
|
208
|
+
provider: 'seedream',
|
|
209
|
+
modelId: 'seedream-v2',
|
|
210
|
+
displayName: 'Seedream v2',
|
|
211
|
+
capabilities: ['txt2img', 'img2img'],
|
|
212
|
+
maxResolution: 1024,
|
|
213
|
+
supportedAspectRatios: ['1:1', '16:9', '9:16'],
|
|
214
|
+
features: {
|
|
215
|
+
textRendering: false,
|
|
216
|
+
facePreservation: false,
|
|
217
|
+
maskInpainting: false,
|
|
218
|
+
referenceConditioning: false,
|
|
219
|
+
controlnet: false,
|
|
220
|
+
},
|
|
221
|
+
estimatedCostUSD: 0.015,
|
|
222
|
+
pricingSource: 'seedream API',
|
|
223
|
+
verifiedDate: '2026-08-20',
|
|
224
|
+
tier: 'economy',
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns the entire capability registry with metadata.
|
|
230
|
+
*/
|
|
231
|
+
export function getCapabilityRegistry() {
|
|
232
|
+
return [...CAPABILITY_REGISTRY]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Filter models based on required operations and features.
|
|
237
|
+
* Unknown capabilities are strictly treated as unsupported.
|
|
238
|
+
*/
|
|
239
|
+
export function filterModelsByCapabilities(requirements = {}, options = {}) {
|
|
240
|
+
const {
|
|
241
|
+
operations = [],
|
|
242
|
+
textRendering = false,
|
|
243
|
+
facePreservation = false,
|
|
244
|
+
maskInpainting = false,
|
|
245
|
+
referenceConditioning = false,
|
|
246
|
+
controlnet = false,
|
|
247
|
+
minResolution = 0,
|
|
248
|
+
aspectRatio = null,
|
|
249
|
+
} = requirements
|
|
250
|
+
|
|
251
|
+
const maxCost = requirements.maxCost ?? requirements.maxCostUSD ?? Infinity
|
|
252
|
+
|
|
253
|
+
const configuredProviders = Array.isArray(options.configuredProviders)
|
|
254
|
+
? options.configuredProviders
|
|
255
|
+
: (Array.isArray(requirements.configuredProviders) ? requirements.configuredProviders : null)
|
|
256
|
+
|
|
257
|
+
return CAPABILITY_REGISTRY.map((model) => {
|
|
258
|
+
const missing = []
|
|
259
|
+
|
|
260
|
+
if (configuredProviders && !configuredProviders.includes(model.provider)) {
|
|
261
|
+
missing.push(`provider_unconfigured:${model.provider}`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const op of operations) {
|
|
265
|
+
if (!model.capabilities.includes(op)) {
|
|
266
|
+
missing.push(`operation:${op}`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (textRendering && !model.features.textRendering) {
|
|
271
|
+
missing.push('feature:textRendering')
|
|
272
|
+
}
|
|
273
|
+
if (facePreservation && !model.features.facePreservation) {
|
|
274
|
+
missing.push('feature:facePreservation')
|
|
275
|
+
}
|
|
276
|
+
if (maskInpainting && !model.features.maskInpainting) {
|
|
277
|
+
missing.push('feature:maskInpainting')
|
|
278
|
+
}
|
|
279
|
+
if (referenceConditioning && !model.features.referenceConditioning) {
|
|
280
|
+
missing.push('feature:referenceConditioning')
|
|
281
|
+
}
|
|
282
|
+
if (controlnet && !model.features.controlnet) {
|
|
283
|
+
missing.push('feature:controlnet')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (minResolution > 0 && model.maxResolution < minResolution) {
|
|
287
|
+
missing.push(`resolution:${model.maxResolution}<${minResolution}`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (aspectRatio && !model.supportedAspectRatios.includes(aspectRatio)) {
|
|
291
|
+
missing.push(`aspectRatio:${aspectRatio}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (model.estimatedCostUSD > maxCost) {
|
|
295
|
+
missing.push(`cost:${model.estimatedCostUSD}>${maxCost}`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
...model,
|
|
300
|
+
compatible: missing.length === 0,
|
|
301
|
+
missingCapabilities: missing,
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Evaluates requirements and recommends the best model.
|
|
308
|
+
*/
|
|
309
|
+
export function recommendModelForTask(requirements = {}, options = {}) {
|
|
310
|
+
const evaluated = filterModelsByCapabilities(requirements, options)
|
|
311
|
+
const compatible = evaluated.filter((m) => m.compatible)
|
|
312
|
+
|
|
313
|
+
if (compatible.length === 0) {
|
|
314
|
+
const partial = [...evaluated].sort((a, b) => a.missingCapabilities.length - b.missingCapabilities.length)
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
recommended: null,
|
|
318
|
+
message: 'No model in registry completely satisfies all specified task requirements.',
|
|
319
|
+
alternatives: partial.slice(0, 3).map((m) => ({
|
|
320
|
+
provider: m.provider,
|
|
321
|
+
modelId: m.modelId,
|
|
322
|
+
displayName: m.displayName,
|
|
323
|
+
missing: m.missingCapabilities,
|
|
324
|
+
estimatedCostUSD: m.estimatedCostUSD,
|
|
325
|
+
})),
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Rank compatible models:
|
|
330
|
+
// 1. Configured providers preferred
|
|
331
|
+
// 2. Cost-effectiveness
|
|
332
|
+
// 3. Higher max resolution
|
|
333
|
+
compatible.sort((a, b) => {
|
|
334
|
+
if (a.estimatedCostUSD !== b.estimatedCostUSD) {
|
|
335
|
+
return a.estimatedCostUSD - b.estimatedCostUSD
|
|
336
|
+
}
|
|
337
|
+
return b.maxResolution - a.maxResolution
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
const best = compatible[0]
|
|
341
|
+
const reasons = []
|
|
342
|
+
if (best.estimatedCostUSD === 0) reasons.push('Zero API cost (local backend)')
|
|
343
|
+
else reasons.push(`Estimated cost: $${best.estimatedCostUSD.toFixed(3)}`)
|
|
344
|
+
if (best.features.textRendering && requirements.textRendering) reasons.push('Native high-precision text rendering')
|
|
345
|
+
if (best.features.facePreservation && requirements.facePreservation) reasons.push('Facial identity retention supported')
|
|
346
|
+
if (best.features.maskInpainting && requirements.maskInpainting) reasons.push('Pixel-accurate mask inpainting supported')
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
ok: true,
|
|
350
|
+
recommended: best,
|
|
351
|
+
recommendationReason: reasons.join('; '),
|
|
352
|
+
allCompatibleCount: compatible.length,
|
|
353
|
+
alternatives: compatible.slice(1, 4).map((m) => ({
|
|
354
|
+
provider: m.provider,
|
|
355
|
+
modelId: m.modelId,
|
|
356
|
+
displayName: m.displayName,
|
|
357
|
+
estimatedCostUSD: m.estimatedCostUSD,
|
|
358
|
+
})),
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Pre-flight validation before provider dispatch (#434).
|
|
364
|
+
* Rejects unsupported capabilities BEFORE submitting to provider to avoid wasting credits or losing inputs.
|
|
365
|
+
*/
|
|
366
|
+
export function validateModelCapability(provider, modelId, requestedFeatures = {}) {
|
|
367
|
+
const model = CAPABILITY_REGISTRY.find(
|
|
368
|
+
(m) => m.provider === provider && (m.modelId === modelId || !modelId)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if (!model) {
|
|
372
|
+
// Unknown model/provider: return strict unknown warning
|
|
373
|
+
return {
|
|
374
|
+
valid: false,
|
|
375
|
+
status: 'unknown_model',
|
|
376
|
+
reason: `Provider "${provider}" with model "${modelId || 'default'}" is not documented in capability registry.`,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const {
|
|
381
|
+
operation,
|
|
382
|
+
requiresMask,
|
|
383
|
+
requiresReference,
|
|
384
|
+
requiresFacePreservation,
|
|
385
|
+
requiresTextRender,
|
|
386
|
+
} = requestedFeatures
|
|
387
|
+
|
|
388
|
+
if (operation && !model.capabilities.includes(operation)) {
|
|
389
|
+
return {
|
|
390
|
+
valid: false,
|
|
391
|
+
status: 'unsupported_operation',
|
|
392
|
+
reason: `Operation "${operation}" is not supported by model "${model.displayName}" (${model.provider}).`,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (requiresMask && !model.features.maskInpainting) {
|
|
397
|
+
return {
|
|
398
|
+
valid: false,
|
|
399
|
+
status: 'unsupported_feature',
|
|
400
|
+
reason: `Mask inpainting is requested but not supported by "${model.displayName}". Discarding mask would corrupt generation.`,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (requiresReference && !model.features.referenceConditioning && !model.capabilities.includes('img2img')) {
|
|
405
|
+
return {
|
|
406
|
+
valid: false,
|
|
407
|
+
status: 'unsupported_feature',
|
|
408
|
+
reason: `Reference image conditioning is requested but not supported by "${model.displayName}".`,
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (requiresFacePreservation && !model.features.facePreservation) {
|
|
413
|
+
return {
|
|
414
|
+
valid: false,
|
|
415
|
+
status: 'unsupported_feature',
|
|
416
|
+
reason: `Face preservation identity transfer is requested but not supported by "${model.displayName}".`,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (requiresTextRender && !model.features.textRendering) {
|
|
421
|
+
return {
|
|
422
|
+
valid: false,
|
|
423
|
+
status: 'unsupported_feature',
|
|
424
|
+
reason: `High-fidelity text rendering is required but "${model.displayName}" lacks native text layout capabilities.`,
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
valid: true,
|
|
430
|
+
status: 'supported',
|
|
431
|
+
model,
|
|
432
|
+
}
|
|
433
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -360,6 +360,7 @@ window.__ModuleLoader__.load({
|
|
|
360
360
|
{ field: 'toolsetProcessing', spec: booleanField('toolsetProcessing'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetProcessing', hintKey: 'f.toolsetProcessingHint', tab: 'general' },
|
|
361
361
|
{ field: 'toolsetFrontend', spec: booleanField('toolsetFrontend'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetFrontend', hintKey: 'f.toolsetFrontendHint', tab: 'general' },
|
|
362
362
|
{ field: 'toolsetCreative', spec: booleanField('toolsetCreative'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetCreative', hintKey: 'f.toolsetCreativeHint', tab: 'general' },
|
|
363
|
+
{ field: 'activeProjectProfile', spec: selectField('activeProjectProfile', ['', 'cyberpunk_neon', 'minimal_nordic', 'corporate_editorial']), kind: 'select', options: ['', 'cyberpunk_neon', 'minimal_nordic', 'corporate_editorial'], labelKey: 'f.activeProjectProfile', hintKey: 'f.activeProjectProfileHint', tab: 'general' },
|
|
363
364
|
|
|
364
365
|
// Provider: FAL
|
|
365
366
|
{ field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'], tab: 'provider' },
|
|
@@ -2878,6 +2879,8 @@ window.__ModuleLoader__.load({
|
|
|
2878
2879
|
'vault.title': 'Asset Browser & Vault',
|
|
2879
2880
|
'f.comfyTemplate': 'ComfyUI Workflow Template',
|
|
2880
2881
|
'f.comfyTemplateHint': 'Pre-vetted workflow template (e.g. SDXL txt2img, img2img, InstantID face transfer, RealESRGAN upscaler).',
|
|
2882
|
+
'f.activeProjectProfile': 'Active Project Profile',
|
|
2883
|
+
'f.activeProjectProfileHint': 'Select a saved visual profile (palette, style, format) to consistently apply across generations (#435).',
|
|
2881
2884
|
}
|
|
2882
2885
|
const zh = {
|
|
2883
2886
|
'card.actionRemoveBg': '移除背景',
|
|
@@ -3187,6 +3190,8 @@ window.__ModuleLoader__.load({
|
|
|
3187
3190
|
'vault.title': '图像素材库与浏览器',
|
|
3188
3191
|
'f.comfyTemplate': 'ComfyUI 工作流模板',
|
|
3189
3192
|
'f.comfyTemplateHint': '经过验证的预设工作流模板(例如 SDXL 文生图、图生图、InstantID 人脸迁移、RealESRGAN 放大器)。',
|
|
3193
|
+
'f.activeProjectProfile': '活动项目视觉配置',
|
|
3194
|
+
'f.activeProjectProfileHint': '选择已保存的项目视觉配置(调色板、风格、格式),跨多次生成统一应用 (#435)。',
|
|
3190
3195
|
}
|
|
3191
3196
|
// Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291, #295)
|
|
3192
3197
|
const inject = ['slots', 'locale', 'sessions']
|
package/lib/config-schema.js
CHANGED
|
@@ -220,6 +220,11 @@ export const Config = z.object({
|
|
|
220
220
|
.description('Vetted ComfyUI workflow template identifier (e.g. "txt2img_sdxl", "img2img_sdxl", "face_instantid", "upscale_esrgan").')
|
|
221
221
|
.default('')
|
|
222
222
|
.volatile(),
|
|
223
|
+
activeProjectProfile: z
|
|
224
|
+
.string()
|
|
225
|
+
.description('Active project visual profile for continuous multi-image styling (#435). One of: "", "cyberpunk_neon", "minimal_nordic", "corporate_editorial".')
|
|
226
|
+
.default('')
|
|
227
|
+
.volatile(),
|
|
223
228
|
comfyWorkflowJson: z
|
|
224
229
|
.string()
|
|
225
230
|
.description('Custom ComfyUI API workflow JSON template with {{prompt}}, {{seed}}, etc. placeholders.')
|
package/lib/index.js
CHANGED
|
@@ -50,6 +50,8 @@ import { clearAllAnchors } from './anchor-helpers.js'
|
|
|
50
50
|
import { clearSpendReservations } from './cost-meter.js'
|
|
51
51
|
import { getAllRecipes, getRecipe } from './recipes.js'
|
|
52
52
|
import { getComfyTemplates, getComfyTemplate, createComfyDependencyValidator } from './comfy-catalog.js'
|
|
53
|
+
import { getCapabilityRegistry, filterModelsByCapabilities, recommendModelForTask } from './capabilities.js'
|
|
54
|
+
import { getAllProjectProfiles, saveCustomProfile, deleteCustomProfile } from './project-profiles.js'
|
|
53
55
|
import { resetLoopGuard, getLoopGuardState } from './loop-guard.js'
|
|
54
56
|
import { Config, plainConfig, volatileConfig, publicConfig, ensureConfig } from './config-schema.js'
|
|
55
57
|
import { resolveSource } from './resolve-source.js'
|
|
@@ -465,6 +467,49 @@ export function apply(ctx, rawConfig) {
|
|
|
465
467
|
},
|
|
466
468
|
}), 'dsh-image-gen: comfy templates route')
|
|
467
469
|
|
|
470
|
+
// Capabilities route (#434)
|
|
471
|
+
ctx.effect(() => ctx.webServer.register({
|
|
472
|
+
kind: 'exact',
|
|
473
|
+
path: '/dsh-image-gen/capabilities',
|
|
474
|
+
handler: async (req, res) => {
|
|
475
|
+
if (req.method !== 'GET') {
|
|
476
|
+
res.writeHead(405, { 'Content-Type': 'application/json' })
|
|
477
|
+
res.end(JSON.stringify({ error: 'GET only' }))
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
481
|
+
res.end(JSON.stringify({ ok: true, registry: getCapabilityRegistry() }))
|
|
482
|
+
},
|
|
483
|
+
}), 'dsh-image-gen: capabilities route')
|
|
484
|
+
|
|
485
|
+
// Project profiles route (#435)
|
|
486
|
+
ctx.effect(() => ctx.webServer.register({
|
|
487
|
+
kind: 'exact',
|
|
488
|
+
path: '/dsh-image-gen/profiles',
|
|
489
|
+
handler: async (req, res) => {
|
|
490
|
+
if (req.method === 'GET') {
|
|
491
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
492
|
+
res.end(JSON.stringify({ ok: true, profiles: getAllProjectProfiles() }))
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
if (req.method === 'POST') {
|
|
496
|
+
try {
|
|
497
|
+
const body = await readBoundedRequestBody(req, 1024 * 1024)
|
|
498
|
+
const data = JSON.parse(body.toString('utf8'))
|
|
499
|
+
const saved = saveCustomProfile(data)
|
|
500
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
501
|
+
res.end(JSON.stringify({ ok: true, profile: saved }))
|
|
502
|
+
} catch (err) {
|
|
503
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
504
|
+
res.end(JSON.stringify({ ok: false, error: err.message }))
|
|
505
|
+
}
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
res.writeHead(405, { 'Content-Type': 'application/json' })
|
|
509
|
+
res.end(JSON.stringify({ error: 'GET or POST only' }))
|
|
510
|
+
},
|
|
511
|
+
}), 'dsh-image-gen: project profiles route')
|
|
512
|
+
|
|
468
513
|
// Live events route (#379)
|
|
469
514
|
registerLiveProgressRoutes(ctx)
|
|
470
515
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Visual Project & Series Profiles (#435)
|
|
7
|
+
* Manages persistent project visual profiles with explicit scope,
|
|
8
|
+
* parameter cascade (defaults -> profile -> request), and immutable history tagging.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const BUILTIN_PROJECT_PROFILES = [
|
|
12
|
+
{
|
|
13
|
+
id: 'cyberpunk_neon',
|
|
14
|
+
name: 'Cyberpunk Neon',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
description: 'High contrast neon lighting and dark futuristic styling.',
|
|
17
|
+
palette: ['#00f0ff', '#ff003c', '#ffe600', '#0a0a12'],
|
|
18
|
+
stylePrompt: 'cyberpunk aesthetic, volumetric atmospheric lighting, raytracing, sharp neon accents, highly detailed 8k',
|
|
19
|
+
negativePrompt: 'washed out, vintage, pastel, desaturated, noisy',
|
|
20
|
+
format: { size: '1024x1024', aspect: '1:1', format: 'webp' },
|
|
21
|
+
rules: {
|
|
22
|
+
background: 'dark cityscapes or moody interior',
|
|
23
|
+
typography: 'clean monospace or futuristic geometric',
|
|
24
|
+
},
|
|
25
|
+
allowedModels: ['fal-ai/flux/dev', 'sdxl_base'],
|
|
26
|
+
builtin: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'minimal_nordic',
|
|
30
|
+
name: 'Scandinavian Minimalist',
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
description: 'Warm daylight, matte textures, and understated earthy tones.',
|
|
33
|
+
palette: ['#f4f1de', '#e07a5f', '#3d405b', '#81b29a'],
|
|
34
|
+
stylePrompt: 'scandinavian minimalism, clean composition, soft natural daylight, matte surfaces, earthy organic elegance',
|
|
35
|
+
negativePrompt: 'cluttered, neon, heavy shadows, oversaturated, glossy plastic',
|
|
36
|
+
format: { size: '1024x1024', aspect: '1:1', format: 'png' },
|
|
37
|
+
rules: {
|
|
38
|
+
background: 'plain textured warm neutral',
|
|
39
|
+
typography: 'light grotesque sans',
|
|
40
|
+
},
|
|
41
|
+
allowedModels: ['fal-ai/flux/dev', 'imagen-3.0-generate-002'],
|
|
42
|
+
builtin: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'corporate_editorial',
|
|
46
|
+
name: 'Corporate Editorial',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
description: 'High-end business magazine photography with crisp studio lighting.',
|
|
49
|
+
palette: ['#0f172a', '#3b82f6', '#64748b', '#f8fafc'],
|
|
50
|
+
stylePrompt: 'high-end corporate magazine editorial, crisp balanced studio lighting, premium commercial photography',
|
|
51
|
+
negativePrompt: 'anime, cartoon, oversaturated, exaggerated distortions, amateur',
|
|
52
|
+
format: { size: '1024x1024', aspect: '16:9', format: 'webp' },
|
|
53
|
+
rules: {
|
|
54
|
+
background: 'architectural soft blur or pristine studio',
|
|
55
|
+
typography: 'editorial serif / humanistic sans',
|
|
56
|
+
},
|
|
57
|
+
allowedModels: ['fal-ai/flux/dev', 'black-forest-labs/flux-1.1-pro'],
|
|
58
|
+
builtin: true,
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
export function getProfilesStoragePath(customPath) {
|
|
63
|
+
if (customPath) return customPath
|
|
64
|
+
if (process.env.DSH_PROJECT_PROFILES_PATH) return process.env.DSH_PROJECT_PROFILES_PATH
|
|
65
|
+
const home = os.homedir() || '/tmp'
|
|
66
|
+
return path.join(home, '.dsh', 'dsh-image-gen', 'project-profiles.json')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadCustomProfiles(storagePath) {
|
|
70
|
+
const filePath = getProfilesStoragePath(storagePath)
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.existsSync(filePath)) return []
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
74
|
+
const parsed = JSON.parse(content)
|
|
75
|
+
return Array.isArray(parsed) ? parsed : []
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function saveCustomProfile(profile, storagePath) {
|
|
82
|
+
if (!profile || typeof profile !== 'object') {
|
|
83
|
+
throw new Error('Invalid project profile: object required')
|
|
84
|
+
}
|
|
85
|
+
if (!profile.id || typeof profile.id !== 'string') {
|
|
86
|
+
throw new Error('Project profile requires a string "id"')
|
|
87
|
+
}
|
|
88
|
+
if (!profile.name || typeof profile.name !== 'string') {
|
|
89
|
+
throw new Error('Project profile requires a string "name"')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const filePath = getProfilesStoragePath(storagePath)
|
|
93
|
+
const dir = path.dirname(filePath)
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const existing = loadCustomProfiles(storagePath)
|
|
99
|
+
const index = existing.findIndex((p) => p.id === profile.id)
|
|
100
|
+
const prepared = {
|
|
101
|
+
...profile,
|
|
102
|
+
version: profile.version || '1.0.0',
|
|
103
|
+
builtin: false,
|
|
104
|
+
updatedAt: new Date().toISOString(),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (index >= 0) {
|
|
108
|
+
existing[index] = prepared
|
|
109
|
+
} else {
|
|
110
|
+
existing.push(prepared)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), 'utf8')
|
|
114
|
+
return prepared
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function deleteCustomProfile(id, storagePath) {
|
|
118
|
+
const filePath = getProfilesStoragePath(storagePath)
|
|
119
|
+
const existing = loadCustomProfiles(storagePath)
|
|
120
|
+
const filtered = existing.filter((p) => p.id !== id)
|
|
121
|
+
if (filtered.length === existing.length) {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
fs.writeFileSync(filePath, JSON.stringify(filtered, null, 2), 'utf8')
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getAllProjectProfiles(storagePath) {
|
|
129
|
+
const custom = loadCustomProfiles(storagePath)
|
|
130
|
+
const customIds = new Set(custom.map((c) => c.id))
|
|
131
|
+
const builtins = BUILTIN_PROJECT_PROFILES.filter((b) => !customIds.has(b.id))
|
|
132
|
+
return [...builtins, ...custom]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getProjectProfileById(id, storagePath) {
|
|
136
|
+
if (!id) return null
|
|
137
|
+
const all = getAllProjectProfiles(storagePath)
|
|
138
|
+
return all.find((p) => p.id === id) || null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parameter resolution cascade (#435):
|
|
143
|
+
* Priority: Request parameters > Project Profile > System Defaults.
|
|
144
|
+
* Returns effective params, provenance mapping, and immutable profile tag for history.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveEffectiveParams(requestParams = {}, profileId = null, storagePath = null) {
|
|
147
|
+
const profile = profileId ? getProjectProfileById(profileId, storagePath) : null
|
|
148
|
+
const provenance = {}
|
|
149
|
+
const effective = {}
|
|
150
|
+
|
|
151
|
+
// 1. Defaults
|
|
152
|
+
const defaults = {
|
|
153
|
+
size: '1024x1024',
|
|
154
|
+
format: 'webp',
|
|
155
|
+
negativePrompt: '',
|
|
156
|
+
steps: 25,
|
|
157
|
+
cfg: 7,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const [key, val] of Object.entries(defaults)) {
|
|
161
|
+
effective[key] = val
|
|
162
|
+
provenance[key] = 'default'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2. Profile layer
|
|
166
|
+
if (profile) {
|
|
167
|
+
if (profile.format?.size) {
|
|
168
|
+
effective.size = profile.format.size
|
|
169
|
+
provenance.size = 'profile'
|
|
170
|
+
}
|
|
171
|
+
if (profile.format?.format) {
|
|
172
|
+
effective.format = profile.format.format
|
|
173
|
+
provenance.format = 'profile'
|
|
174
|
+
}
|
|
175
|
+
if (profile.negativePrompt) {
|
|
176
|
+
effective.negativePrompt = profile.negativePrompt
|
|
177
|
+
provenance.negativePrompt = 'profile'
|
|
178
|
+
}
|
|
179
|
+
if (profile.stylePrompt) {
|
|
180
|
+
effective.stylePrompt = profile.stylePrompt
|
|
181
|
+
provenance.stylePrompt = 'profile'
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(profile.palette) && profile.palette.length > 0) {
|
|
184
|
+
effective.palette = [...profile.palette]
|
|
185
|
+
provenance.palette = 'profile'
|
|
186
|
+
}
|
|
187
|
+
if (profile.rules) {
|
|
188
|
+
effective.rules = { ...profile.rules }
|
|
189
|
+
provenance.rules = 'profile'
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3. Request layer (highest precedence)
|
|
194
|
+
for (const [key, val] of Object.entries(requestParams)) {
|
|
195
|
+
if (val !== undefined && val !== null) {
|
|
196
|
+
if (key === 'prompt') {
|
|
197
|
+
// Synthesize prompt with profile style if available
|
|
198
|
+
if (effective.stylePrompt) {
|
|
199
|
+
effective.prompt = `${val}, ${effective.stylePrompt}`
|
|
200
|
+
provenance.prompt = 'request+profile'
|
|
201
|
+
} else {
|
|
202
|
+
effective.prompt = val
|
|
203
|
+
provenance.prompt = 'request'
|
|
204
|
+
}
|
|
205
|
+
} else if (key === 'negativePrompt') {
|
|
206
|
+
if (effective.negativePrompt && val) {
|
|
207
|
+
effective.negativePrompt = `${effective.negativePrompt}, ${val}`
|
|
208
|
+
provenance.negativePrompt = 'request+profile'
|
|
209
|
+
} else {
|
|
210
|
+
effective.negativePrompt = val || effective.negativePrompt
|
|
211
|
+
provenance.negativePrompt = 'request'
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
effective[key] = val
|
|
215
|
+
provenance[key] = 'request'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Immutable applied profile metadata snapshot for history
|
|
221
|
+
const appliedProfile = profile
|
|
222
|
+
? {
|
|
223
|
+
id: profile.id,
|
|
224
|
+
name: profile.name,
|
|
225
|
+
version: profile.version || '1.0.0',
|
|
226
|
+
}
|
|
227
|
+
: null
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
effectiveParams: effective,
|
|
231
|
+
provenance,
|
|
232
|
+
appliedProfile,
|
|
233
|
+
}
|
|
234
|
+
}
|
package/lib/register-tools.js
CHANGED
|
@@ -20,6 +20,8 @@ import { registerCharacterSheetTools } from './tools/character-sheet.js'
|
|
|
20
20
|
import { registerVisionOcrTools } from './tools/vision-ocr.js'
|
|
21
21
|
import { registerDiagramTools } from './tools/diagram.js'
|
|
22
22
|
import { registerRecipeTools } from './tools/recipes.js'
|
|
23
|
+
import { registerCapabilityTools } from './tools/capabilities.js'
|
|
24
|
+
import { registerProjectProfileTools } from './tools/project-profiles.js'
|
|
23
25
|
import {
|
|
24
26
|
createLiveProgressSession,
|
|
25
27
|
publishLiveProgress,
|
|
@@ -149,6 +151,12 @@ export function registerAllTools(ctx, deps) {
|
|
|
149
151
|
if (Boolean(sets.recipes)) {
|
|
150
152
|
registerRecipeTools(targetCtx, { ...deps, toolDispatcher })
|
|
151
153
|
}
|
|
154
|
+
if (Boolean(sets.capabilities)) {
|
|
155
|
+
registerCapabilityTools(targetCtx, deps)
|
|
156
|
+
}
|
|
157
|
+
if (Boolean(sets.profiles)) {
|
|
158
|
+
registerProjectProfileTools(targetCtx, deps)
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
function dispose() {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCapabilityRegistry,
|
|
3
|
+
recommendModelForTask,
|
|
4
|
+
validateModelCapability,
|
|
5
|
+
} from '../capabilities.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registers capability-based model selection tools (#434).
|
|
9
|
+
*/
|
|
10
|
+
export function registerCapabilityTools(ctx, deps = {}) {
|
|
11
|
+
ctx.tools.register({
|
|
12
|
+
name: 'select_image_model',
|
|
13
|
+
description: 'Recommends the best provider and model based on required task capabilities (text rendering, inpainting, face preservation, cost limits).',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
operations: {
|
|
19
|
+
type: 'array',
|
|
20
|
+
items: { type: 'string' },
|
|
21
|
+
description: 'Required operations, e.g. ["txt2img", "inpaint", "img2img"].',
|
|
22
|
+
},
|
|
23
|
+
textRendering: {
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
description: 'Whether native high-precision text rendering is required.',
|
|
26
|
+
},
|
|
27
|
+
facePreservation: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Whether facial identity likeness preservation is required.',
|
|
30
|
+
},
|
|
31
|
+
maskInpainting: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
description: 'Whether pixel-accurate inpainting mask support is required.',
|
|
34
|
+
},
|
|
35
|
+
referenceConditioning: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Whether reference image guidance is required.',
|
|
38
|
+
},
|
|
39
|
+
controlnet: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'Whether structural ControlNet guidance is required.',
|
|
42
|
+
},
|
|
43
|
+
minResolution: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description: 'Minimum resolution in pixels (e.g. 1024 or 2048).',
|
|
46
|
+
},
|
|
47
|
+
maxCostUSD: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
description: 'Maximum estimated cost in USD per generation.',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
output: {
|
|
54
|
+
render(args, value) {
|
|
55
|
+
if (!value?.ok) {
|
|
56
|
+
return [{ type: 'text', text: `Capability selection: ${value?.message || 'No matching model found.'}` }]
|
|
57
|
+
}
|
|
58
|
+
const rec = value.recommended
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: `Recommended Model: ${rec.displayName} (${rec.provider}/${rec.modelId})\nReason: ${value.recommendationReason}\nEstimated Cost: $${rec.estimatedCostUSD} (Source: ${rec.pricingSource})`,
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
async execute(args) {
|
|
68
|
+
const requirements = {
|
|
69
|
+
operations: args.operations || [],
|
|
70
|
+
textRendering: Boolean(args.textRendering),
|
|
71
|
+
facePreservation: Boolean(args.facePreservation),
|
|
72
|
+
maskInpainting: Boolean(args.maskInpainting),
|
|
73
|
+
referenceConditioning: Boolean(args.referenceConditioning),
|
|
74
|
+
controlnet: Boolean(args.controlnet),
|
|
75
|
+
minResolution: args.minResolution || 0,
|
|
76
|
+
maxCost: args.maxCostUSD || Infinity,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check configured provider list from deps or runtime
|
|
80
|
+
const configuredProviders = deps.cfg?.provider ? [deps.cfg.provider] : null
|
|
81
|
+
return recommendModelForTask(requirements, { configuredProviders })
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
ctx.tools.register({
|
|
86
|
+
name: 'validate_model_features',
|
|
87
|
+
description: 'Pre-flight check if a model or provider supports specific generation features before running the task.',
|
|
88
|
+
parameters: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
properties: {
|
|
92
|
+
provider: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Provider name (fal, gemini, replicate, local, seedream).',
|
|
95
|
+
},
|
|
96
|
+
modelId: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Specific model identifier.',
|
|
99
|
+
},
|
|
100
|
+
operation: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Requested operation (txt2img, img2img, inpaint).',
|
|
103
|
+
},
|
|
104
|
+
requiresMask: {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
description: 'Whether operation requires inpainting mask.',
|
|
107
|
+
},
|
|
108
|
+
requiresReference: {
|
|
109
|
+
type: 'boolean',
|
|
110
|
+
description: 'Whether operation requires reference image.',
|
|
111
|
+
},
|
|
112
|
+
requiresFacePreservation: {
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'Whether operation requires face preservation.',
|
|
115
|
+
},
|
|
116
|
+
requiresTextRender: {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
description: 'Whether operation requires text rendering.',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['provider'],
|
|
122
|
+
},
|
|
123
|
+
output: {
|
|
124
|
+
render(args, value) {
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: value.valid
|
|
129
|
+
? `Validation PASSED for ${args.provider}: ${value.model?.displayName}`
|
|
130
|
+
: `Validation FAILED (${value.status}): ${value.reason}`,
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
async execute(args) {
|
|
136
|
+
return validateModelCapability(args.provider, args.modelId, args)
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAllProjectProfiles,
|
|
3
|
+
getProjectProfileById,
|
|
4
|
+
saveCustomProfile,
|
|
5
|
+
deleteCustomProfile,
|
|
6
|
+
resolveEffectiveParams,
|
|
7
|
+
} from '../project-profiles.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers project visual profile management tools (#435).
|
|
11
|
+
*/
|
|
12
|
+
export function registerProjectProfileTools(ctx, deps = {}) {
|
|
13
|
+
ctx.tools.register({
|
|
14
|
+
name: 'manage_project_profiles',
|
|
15
|
+
description: 'Manages visual project and series profiles (list, get, save, delete) for consistent styling across generation sets.',
|
|
16
|
+
parameters: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: {
|
|
20
|
+
action: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['list', 'get', 'save', 'delete'],
|
|
23
|
+
description: 'Action to perform: list all profiles, get by id, save (create/update), or delete.',
|
|
24
|
+
},
|
|
25
|
+
id: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Profile ID for get or delete actions.',
|
|
28
|
+
},
|
|
29
|
+
profile: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
additionalProperties: true,
|
|
32
|
+
description: 'Profile specification object for save action (id, name, palette, stylePrompt, negativePrompt, format, rules).',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['action'],
|
|
36
|
+
},
|
|
37
|
+
output: {
|
|
38
|
+
render(args, value) {
|
|
39
|
+
if (args.action === 'list') {
|
|
40
|
+
const list = Array.isArray(value) ? value : []
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: `Project Profiles (${list.length}):\n` +
|
|
45
|
+
list.map((p) => `- [${p.id}] ${p.name} (v${p.version || '1.0.0'}) ${p.builtin ? '(builtin)' : '(custom)'}`).join('\n'),
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
if (args.action === 'get') {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: value ? `Profile [${value.id}] ${value.name}:\nStyle: "${value.stylePrompt || ''}"\nPalette: ${(value.palette || []).join(', ')}` : 'Profile not found.',
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
if (args.action === 'save') {
|
|
58
|
+
return [{ type: 'text', text: `Saved profile [${value?.id}] ${value?.name} successfully.` }]
|
|
59
|
+
}
|
|
60
|
+
if (args.action === 'delete') {
|
|
61
|
+
return [{ type: 'text', text: value ? `Deleted profile ${args.id}.` : `Profile ${args.id} not found or cannot be deleted.` }]
|
|
62
|
+
}
|
|
63
|
+
return [{ type: 'text', text: JSON.stringify(value) }]
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
async execute(args) {
|
|
67
|
+
const storagePath = deps.profilesPath || null
|
|
68
|
+
if (args.action === 'list') {
|
|
69
|
+
return getAllProjectProfiles(storagePath)
|
|
70
|
+
}
|
|
71
|
+
if (args.action === 'get') {
|
|
72
|
+
return getProjectProfileById(args.id, storagePath)
|
|
73
|
+
}
|
|
74
|
+
if (args.action === 'save') {
|
|
75
|
+
return saveCustomProfile(args.profile, storagePath)
|
|
76
|
+
}
|
|
77
|
+
if (args.action === 'delete') {
|
|
78
|
+
return deleteCustomProfile(args.id, storagePath)
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Unknown action: ${args.action}`)
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
ctx.tools.register({
|
|
85
|
+
name: 'resolve_project_profile_params',
|
|
86
|
+
description: 'Calculates effective generation parameters by cascading request overrides over a project visual profile and system defaults.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
properties: {
|
|
91
|
+
profileId: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Identifier of the project visual profile.',
|
|
94
|
+
},
|
|
95
|
+
prompt: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Specific image request prompt.',
|
|
98
|
+
},
|
|
99
|
+
negativePrompt: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Specific negative prompt overrides.',
|
|
102
|
+
},
|
|
103
|
+
size: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Specific dimensions (e.g. 1024x1024 or 1280x720).',
|
|
106
|
+
},
|
|
107
|
+
format: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Output format (webp, png, jpeg).',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['profileId'],
|
|
113
|
+
},
|
|
114
|
+
output: {
|
|
115
|
+
render(args, value) {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: `Effective Parameters for Profile [${value?.appliedProfile?.name || args.profileId}]:\n` +
|
|
120
|
+
`Prompt: "${value?.effectiveParams?.prompt || ''}"\n` +
|
|
121
|
+
`Negative: "${value?.effectiveParams?.negativePrompt || ''}"\n` +
|
|
122
|
+
`Dimensions: ${value?.effectiveParams?.size} (${value?.effectiveParams?.format})\n` +
|
|
123
|
+
`Provenance: ${JSON.stringify(value?.provenance || {})}`,
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
async execute(args) {
|
|
129
|
+
const storagePath = deps.profilesPath || null
|
|
130
|
+
const requestParams = {
|
|
131
|
+
prompt: args.prompt,
|
|
132
|
+
negativePrompt: args.negativePrompt,
|
|
133
|
+
size: args.size,
|
|
134
|
+
format: args.format,
|
|
135
|
+
}
|
|
136
|
+
return resolveEffectiveParams(requestParams, args.profileId, storagePath)
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.40",
|
|
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",
|