@goodandready/dsh-image-gen 0.10.4 → 0.10.5
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 +7 -0
- package/README.ru.md +4 -0
- package/lib/frontend-assets.js +436 -0
- package/lib/index.js +562 -2
- package/lib/providers.js +50 -2
- package/lib/resolve-image.js +243 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -47,7 +47,110 @@ import {
|
|
|
47
47
|
blendImagesFal,
|
|
48
48
|
} from './providers.js'
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from './resolve-image.js'
|
|
51
|
+
import {
|
|
52
|
+
extractDesignTokens,
|
|
53
|
+
generateCssGradient,
|
|
54
|
+
checkWcagContrast,
|
|
55
|
+
optimizeSvgContent,
|
|
56
|
+
generatePwaIconSuite,
|
|
57
|
+
extractSampleColorsFromBuffer,
|
|
58
|
+
} from './frontend-assets.js'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export { IMAGE_SIZES, OUTPUT_FORMATS, PROVIDER_KEYS, buildSidecar, normalizeMediaType, resolveConversationImage, analyzeImageWithVision }
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Сохраняет сгенерированный ассет в воркспейс, регистрирует в attachments и возвращает Dual-Output (#150).
|
|
66
|
+
*/
|
|
67
|
+
export async function saveAndAttachResult(ctx, exec, cfg, {
|
|
68
|
+
bytes,
|
|
69
|
+
mediaType,
|
|
70
|
+
name,
|
|
71
|
+
stem,
|
|
72
|
+
prompt,
|
|
73
|
+
size,
|
|
74
|
+
format,
|
|
75
|
+
seed,
|
|
76
|
+
provider,
|
|
77
|
+
model,
|
|
78
|
+
cost,
|
|
79
|
+
sourceUrl,
|
|
80
|
+
deliverAs,
|
|
81
|
+
args = {},
|
|
82
|
+
action = 'generated',
|
|
83
|
+
}) {
|
|
84
|
+
const attachment = await ctx.attachments.saveImage({
|
|
85
|
+
data: new Uint8Array(bytes),
|
|
86
|
+
mediaType,
|
|
87
|
+
name,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
91
|
+
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
92
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
93
|
+
await mkdir(outDir, { recursive: true })
|
|
94
|
+
const filePath = path.join(outDir, name)
|
|
95
|
+
await writeFile(filePath, bytes)
|
|
96
|
+
|
|
97
|
+
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
98
|
+
+ '&mt=' + encodeURIComponent(attachment.mediaType)
|
|
99
|
+
+ '&b=' + encodeURIComponent(String(attachment.bytes))
|
|
100
|
+
+ '&w=' + encodeURIComponent(String(attachment.width))
|
|
101
|
+
+ '&h=' + encodeURIComponent(String(attachment.height))
|
|
102
|
+
|
|
103
|
+
await writeFile(
|
|
104
|
+
path.join(outDir, `${stem}.json`),
|
|
105
|
+
JSON.stringify(buildSidecar({
|
|
106
|
+
prompt,
|
|
107
|
+
size,
|
|
108
|
+
format,
|
|
109
|
+
seed,
|
|
110
|
+
provider,
|
|
111
|
+
deliverAs,
|
|
112
|
+
width: attachment.width,
|
|
113
|
+
height: attachment.height,
|
|
114
|
+
mediaType,
|
|
115
|
+
attachmentId: attachment.attachmentId,
|
|
116
|
+
url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
|
|
117
|
+
cost,
|
|
118
|
+
}), null, 2),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const summary = buildDualOutputMarkdown({
|
|
122
|
+
action,
|
|
123
|
+
filePath,
|
|
124
|
+
width: attachment.width,
|
|
125
|
+
height: attachment.height,
|
|
126
|
+
mediaType,
|
|
127
|
+
seed,
|
|
128
|
+
provider,
|
|
129
|
+
model: model || cfg.model || cfg.customModel || 'default',
|
|
130
|
+
cost,
|
|
131
|
+
attachmentId: attachment.attachmentId,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
summary,
|
|
136
|
+
path: filePath,
|
|
137
|
+
url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
|
|
138
|
+
width: attachment.width,
|
|
139
|
+
height: attachment.height,
|
|
140
|
+
seed,
|
|
141
|
+
prompt,
|
|
142
|
+
cost,
|
|
143
|
+
format: mediaType.replace('image/', ''),
|
|
144
|
+
attachment: {
|
|
145
|
+
attachmentId: attachment.attachmentId,
|
|
146
|
+
mediaType: attachment.mediaType,
|
|
147
|
+
bytes: attachment.bytes,
|
|
148
|
+
width: attachment.width,
|
|
149
|
+
height: attachment.height,
|
|
150
|
+
name: attachment.name,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
51
154
|
|
|
52
155
|
export const name = 'dsh-image-gen'
|
|
53
156
|
|
|
@@ -873,7 +976,19 @@ export function apply(ctx, config) {
|
|
|
873
976
|
}
|
|
874
977
|
}
|
|
875
978
|
const first = images[0]
|
|
876
|
-
|
|
979
|
+
const dualOutputSummary = buildDualOutputMarkdown({
|
|
980
|
+
action: 'generated',
|
|
981
|
+
filePath: first.path,
|
|
982
|
+
width: first.width,
|
|
983
|
+
height: first.height,
|
|
984
|
+
mediaType: first.mediaType || ('image/' + (first.format || 'png')),
|
|
985
|
+
seed: first.seed,
|
|
986
|
+
provider,
|
|
987
|
+
model: cfg.model || cfg.customModel || 'default',
|
|
988
|
+
cost: first.cost,
|
|
989
|
+
attachmentId: first.attachment?.attachmentId || 'N/A',
|
|
990
|
+
})
|
|
991
|
+
return { summary: dualOutputSummary, channel: provider, provider, model: cfg.model || cfg.customModel, ...first, images }
|
|
877
992
|
},
|
|
878
993
|
}),
|
|
879
994
|
)
|
|
@@ -1182,6 +1297,252 @@ export function apply(ctx, config) {
|
|
|
1182
1297
|
}),
|
|
1183
1298
|
)
|
|
1184
1299
|
|
|
1300
|
+
// Редактирование существующего изображения (Inpainting / Edits) (#142, #144, #145, #150).
|
|
1301
|
+
ctx.tools.register(
|
|
1302
|
+
defineTool({
|
|
1303
|
+
name: 'edit_image',
|
|
1304
|
+
description:
|
|
1305
|
+
'Edit an existing image using inpainting, targeted modification, or object replacement. '
|
|
1306
|
+
+ 'Describe the desired changes in prompt. '
|
|
1307
|
+
+ 'If image is omitted or "latest", automatically resolves the most recent image from the conversation session (#144). '
|
|
1308
|
+
+ 'Leverages @goodandready/dsh-vision-bridge for pre-edit composition analysis (#145). '
|
|
1309
|
+
+ 'Optionally accepts mask (path or attachment id) and strength (0.1-1.0). Returns safe dual-output (#150).',
|
|
1310
|
+
parameters: {
|
|
1311
|
+
prompt: {
|
|
1312
|
+
type: 'string',
|
|
1313
|
+
required: true,
|
|
1314
|
+
description: 'Detailed description of the modification or edit to perform on the image.',
|
|
1315
|
+
},
|
|
1316
|
+
image: {
|
|
1317
|
+
type: 'string',
|
|
1318
|
+
description: 'Path or attachment id of the source image. Defaults to "latest" to auto-resolve from session history.',
|
|
1319
|
+
},
|
|
1320
|
+
mask: {
|
|
1321
|
+
type: 'string',
|
|
1322
|
+
description: 'Optional path or attachment id of the inpaint mask image.',
|
|
1323
|
+
},
|
|
1324
|
+
strength: {
|
|
1325
|
+
type: 'number',
|
|
1326
|
+
description: 'Denoising / edit strength between 0.1 and 1.0 (default 0.75 for edits, 0.9 for mask inpainting).',
|
|
1327
|
+
},
|
|
1328
|
+
output_name: {
|
|
1329
|
+
type: 'string',
|
|
1330
|
+
description: 'Optional file name stem for the saved edited image.',
|
|
1331
|
+
},
|
|
1332
|
+
output_dir: {
|
|
1333
|
+
type: 'string',
|
|
1334
|
+
description: 'Optional custom directory to write edited image into.',
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
output: {
|
|
1338
|
+
schema: {
|
|
1339
|
+
type: 'object',
|
|
1340
|
+
additionalProperties: true,
|
|
1341
|
+
properties: {
|
|
1342
|
+
summary: { type: 'string' },
|
|
1343
|
+
path: { type: 'string' },
|
|
1344
|
+
url: { type: 'string' },
|
|
1345
|
+
width: { type: 'number' },
|
|
1346
|
+
height: { type: 'number' },
|
|
1347
|
+
seed: { type: 'number' },
|
|
1348
|
+
provider: { type: 'string' },
|
|
1349
|
+
cost: { type: 'number' },
|
|
1350
|
+
format: { type: 'string' },
|
|
1351
|
+
attachment: { type: 'object', additionalProperties: true },
|
|
1352
|
+
visionAnalysis: { type: 'string' },
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
async execute(args, exec) {
|
|
1357
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
1358
|
+
const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
|
|
1359
|
+
const vision = await analyzeImageWithVision(ctx, exec, source)
|
|
1360
|
+
|
|
1361
|
+
let effectivePrompt = args.prompt
|
|
1362
|
+
if (vision.available && vision.summary) {
|
|
1363
|
+
effectivePrompt = `${args.prompt} (Context: modifying ${vision.summary})`
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const provider = cfg.provider || 'fal'
|
|
1367
|
+
const deps = {
|
|
1368
|
+
fetchImpl: ctx.http ? (url, opt) => ctx.http(url, opt) : fetch,
|
|
1369
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
1370
|
+
cfg,
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const seed = Math.floor(Math.random() * 100000)
|
|
1374
|
+
const strength = args.strength ?? (mask ? 0.85 : 0.65)
|
|
1375
|
+
const format = cfg.defaultFormat || 'png'
|
|
1376
|
+
const size = cfg.defaultSize || 'square_hd'
|
|
1377
|
+
|
|
1378
|
+
const job = {
|
|
1379
|
+
prompt: effectivePrompt,
|
|
1380
|
+
size,
|
|
1381
|
+
format,
|
|
1382
|
+
seed,
|
|
1383
|
+
source,
|
|
1384
|
+
mask,
|
|
1385
|
+
strength,
|
|
1386
|
+
provider,
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const providers = makeProviders(deps, job)
|
|
1390
|
+
const fn = providers[provider] || providers.fal || providers.custom
|
|
1391
|
+
const gen = await fn(seed, effectivePrompt)
|
|
1392
|
+
|
|
1393
|
+
const stem = args.output_name ? slugify(args.output_name) : `${source.name}-edit-${seed}`
|
|
1394
|
+
const name = `${stem}.${format}`
|
|
1395
|
+
|
|
1396
|
+
const result = await saveAndAttachResult(ctx, exec, cfg, {
|
|
1397
|
+
bytes: gen.bytes,
|
|
1398
|
+
mediaType: gen.mediaType || 'image/png',
|
|
1399
|
+
name,
|
|
1400
|
+
stem,
|
|
1401
|
+
prompt: args.prompt,
|
|
1402
|
+
size,
|
|
1403
|
+
format,
|
|
1404
|
+
seed: gen.seed ?? seed,
|
|
1405
|
+
provider,
|
|
1406
|
+
model: cfg.model,
|
|
1407
|
+
cost: gen.cost,
|
|
1408
|
+
sourceUrl: gen.sourceUrl,
|
|
1409
|
+
deliverAs: cfg.deliverAs || 'both',
|
|
1410
|
+
args,
|
|
1411
|
+
action: 'edited',
|
|
1412
|
+
})
|
|
1413
|
+
|
|
1414
|
+
if (vision.summary) {
|
|
1415
|
+
result.visionAnalysis = vision.summary
|
|
1416
|
+
}
|
|
1417
|
+
return result
|
|
1418
|
+
},
|
|
1419
|
+
}),
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
// Генерация вариаций существующего изображения (#143, #144, #145, #150).
|
|
1423
|
+
ctx.tools.register(
|
|
1424
|
+
defineTool({
|
|
1425
|
+
name: 'vary_image',
|
|
1426
|
+
description:
|
|
1427
|
+
'Generate subtle or creative variations of an existing image while preserving overall composition and structure. '
|
|
1428
|
+
+ 'Pass variation_strength between 0.1 (subtle difference) and 0.9 (strong reinterpretation, default 0.35). '
|
|
1429
|
+
+ 'If image is omitted or "latest", automatically resolves the most recent image from the conversation session (#144). '
|
|
1430
|
+
+ 'Returns safe dual-output for text-only LLMs (#150).',
|
|
1431
|
+
parameters: {
|
|
1432
|
+
image: {
|
|
1433
|
+
type: 'string',
|
|
1434
|
+
description: 'Path or attachment id of the source image. Defaults to "latest" to auto-resolve from session history.',
|
|
1435
|
+
},
|
|
1436
|
+
prompt: {
|
|
1437
|
+
type: 'string',
|
|
1438
|
+
description: 'Optional prompt to steer the variation towards a specific mood, lighting, or style.',
|
|
1439
|
+
},
|
|
1440
|
+
variation_strength: {
|
|
1441
|
+
type: 'number',
|
|
1442
|
+
description: 'Strength of variation between 0.1 and 0.9. Default: 0.35.',
|
|
1443
|
+
},
|
|
1444
|
+
count: {
|
|
1445
|
+
type: 'integer',
|
|
1446
|
+
description: 'Number of variations to produce (1-4, default 1).',
|
|
1447
|
+
},
|
|
1448
|
+
output_name: {
|
|
1449
|
+
type: 'string',
|
|
1450
|
+
description: 'Optional file name stem for the saved variations.',
|
|
1451
|
+
},
|
|
1452
|
+
output_dir: {
|
|
1453
|
+
type: 'string',
|
|
1454
|
+
description: 'Optional custom directory to write variations into.',
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
output: {
|
|
1458
|
+
schema: {
|
|
1459
|
+
type: 'object',
|
|
1460
|
+
additionalProperties: true,
|
|
1461
|
+
properties: {
|
|
1462
|
+
summary: { type: 'string' },
|
|
1463
|
+
path: { type: 'string' },
|
|
1464
|
+
url: { type: 'string' },
|
|
1465
|
+
width: { type: 'number' },
|
|
1466
|
+
height: { type: 'number' },
|
|
1467
|
+
seed: { type: 'number' },
|
|
1468
|
+
provider: { type: 'string' },
|
|
1469
|
+
cost: { type: 'number' },
|
|
1470
|
+
format: { type: 'string' },
|
|
1471
|
+
attachment: { type: 'object', additionalProperties: true },
|
|
1472
|
+
variations: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
},
|
|
1476
|
+
async execute(args, exec) {
|
|
1477
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
1478
|
+
const vision = await analyzeImageWithVision(ctx, exec, source)
|
|
1479
|
+
|
|
1480
|
+
const provider = cfg.provider || 'fal'
|
|
1481
|
+
const deps = {
|
|
1482
|
+
fetchImpl: ctx.http ? (url, opt) => ctx.http(url, opt) : fetch,
|
|
1483
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
1484
|
+
cfg,
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const count = normalizeCount(args.count || 1)
|
|
1488
|
+
const variationStrength = Math.max(0.05, Math.min(0.95, args.variation_strength ?? 0.35))
|
|
1489
|
+
const baseSeed = Math.floor(Math.random() * 100000)
|
|
1490
|
+
const format = cfg.defaultFormat || 'png'
|
|
1491
|
+
const size = cfg.defaultSize || 'square_hd'
|
|
1492
|
+
|
|
1493
|
+
const promptText = args.prompt || (vision.available ? `variation of ${vision.summary}` : 'high fidelity variation preserving composition')
|
|
1494
|
+
|
|
1495
|
+
const results = []
|
|
1496
|
+
for (let i = 0; i < count; i++) {
|
|
1497
|
+
const currentSeed = baseSeed + i
|
|
1498
|
+
const job = {
|
|
1499
|
+
prompt: promptText,
|
|
1500
|
+
size,
|
|
1501
|
+
format,
|
|
1502
|
+
seed: currentSeed,
|
|
1503
|
+
source,
|
|
1504
|
+
strength: variationStrength,
|
|
1505
|
+
provider,
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const providers = makeProviders(deps, job)
|
|
1509
|
+
const fn = providers[provider] || providers.fal || providers.custom
|
|
1510
|
+
const gen = await fn(currentSeed, promptText)
|
|
1511
|
+
|
|
1512
|
+
const stem = args.output_name
|
|
1513
|
+
? `${slugify(args.output_name)}-var-${i + 1}`
|
|
1514
|
+
: `${source.name}-var-${currentSeed}`
|
|
1515
|
+
const name = `${stem}.${format}`
|
|
1516
|
+
|
|
1517
|
+
const item = await saveAndAttachResult(ctx, exec, cfg, {
|
|
1518
|
+
bytes: gen.bytes,
|
|
1519
|
+
mediaType: gen.mediaType || 'image/png',
|
|
1520
|
+
name,
|
|
1521
|
+
stem,
|
|
1522
|
+
prompt: promptText,
|
|
1523
|
+
size,
|
|
1524
|
+
format,
|
|
1525
|
+
seed: gen.seed ?? currentSeed,
|
|
1526
|
+
provider,
|
|
1527
|
+
model: cfg.model,
|
|
1528
|
+
cost: gen.cost,
|
|
1529
|
+
sourceUrl: gen.sourceUrl,
|
|
1530
|
+
deliverAs: cfg.deliverAs || 'both',
|
|
1531
|
+
args,
|
|
1532
|
+
action: `variation (${variationStrength})`,
|
|
1533
|
+
})
|
|
1534
|
+
results.push(item)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const first = results[0]
|
|
1538
|
+
return {
|
|
1539
|
+
...first,
|
|
1540
|
+
variations: results.map((r) => ({ path: r.path, attachmentId: r.attachment?.attachmentId, seed: r.seed })),
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
}),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1185
1546
|
// Проверка качества изображения (Inspect Image Quality).
|
|
1186
1547
|
ctx.tools.register(
|
|
1187
1548
|
defineTool({
|
|
@@ -1215,6 +1576,205 @@ export function apply(ctx, config) {
|
|
|
1215
1576
|
},
|
|
1216
1577
|
}),
|
|
1217
1578
|
)
|
|
1579
|
+
|
|
1580
|
+
// #172: Генератор дизайн-токенов и CSS/Tailwind палитры по концепту
|
|
1581
|
+
ctx.tools.register(
|
|
1582
|
+
defineTool({
|
|
1583
|
+
name: 'extract_design_tokens',
|
|
1584
|
+
description: 'Extract design tokens (CSS variables, Tailwind colors config, W3C tokens JSON) from an image or palette.',
|
|
1585
|
+
parameters: {
|
|
1586
|
+
image: { type: 'string', description: 'Path or attachment ID of the image to extract colors from.' },
|
|
1587
|
+
colors: { type: 'array', items: { type: 'string' }, description: 'Optional list of hex colors if already known.' },
|
|
1588
|
+
prefix: { type: 'string', description: 'Token naming prefix (default: color).' },
|
|
1589
|
+
},
|
|
1590
|
+
output: {
|
|
1591
|
+
schema: {
|
|
1592
|
+
type: 'object',
|
|
1593
|
+
additionalProperties: true,
|
|
1594
|
+
properties: {
|
|
1595
|
+
tokens: { type: 'object', additionalProperties: true },
|
|
1596
|
+
cssVariables: { type: 'string' },
|
|
1597
|
+
tailwindSnippet: { type: 'string' },
|
|
1598
|
+
w3cTokens: { type: 'object', additionalProperties: true },
|
|
1599
|
+
},
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
async execute(args, exec) {
|
|
1603
|
+
let colors = args.colors
|
|
1604
|
+
if (!colors || colors.length === 0) {
|
|
1605
|
+
if (args.image) {
|
|
1606
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1607
|
+
if (source && source.bytes) {
|
|
1608
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return extractDesignTokens(colors, { prefix: args.prefix })
|
|
1613
|
+
},
|
|
1614
|
+
}),
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
// #174: Конвертер фоновых изображений в легковесные CSS Mesh Gradients
|
|
1618
|
+
ctx.tools.register(
|
|
1619
|
+
defineTool({
|
|
1620
|
+
name: 'image_to_css_gradient',
|
|
1621
|
+
description: 'Convert an image background or color palette into a lightweight pure CSS gradient (< 1KB).',
|
|
1622
|
+
parameters: {
|
|
1623
|
+
image: { type: 'string', description: 'Path or attachment ID of the image.' },
|
|
1624
|
+
colors: { type: 'array', items: { type: 'string' }, description: 'Optional explicit hex colors for the gradient.' },
|
|
1625
|
+
type: { type: 'string', enum: ['mesh', 'linear', 'radial'], description: 'Gradient type: mesh, linear, or radial (default: mesh).' },
|
|
1626
|
+
},
|
|
1627
|
+
output: {
|
|
1628
|
+
schema: {
|
|
1629
|
+
type: 'object',
|
|
1630
|
+
additionalProperties: true,
|
|
1631
|
+
properties: {
|
|
1632
|
+
type: { type: 'string' },
|
|
1633
|
+
fallbackColor: { type: 'string' },
|
|
1634
|
+
gradientCss: { type: 'string' },
|
|
1635
|
+
completeStyle: { type: 'string' },
|
|
1636
|
+
byteSize: { type: 'number' },
|
|
1637
|
+
},
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1640
|
+
async execute(args, exec) {
|
|
1641
|
+
let colors = args.colors
|
|
1642
|
+
if (!colors || colors.length === 0) {
|
|
1643
|
+
if (args.image) {
|
|
1644
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1645
|
+
if (source && source.bytes) {
|
|
1646
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return generateCssGradient(colors, args.type || 'mesh')
|
|
1651
|
+
},
|
|
1652
|
+
}),
|
|
1653
|
+
)
|
|
1654
|
+
|
|
1655
|
+
// #175: Проверка контрастности фона под текст по WCAG 2.1
|
|
1656
|
+
ctx.tools.register(
|
|
1657
|
+
defineTool({
|
|
1658
|
+
name: 'check_image_contrast',
|
|
1659
|
+
description: 'Check image background contrast ratio against text color according to WCAG 2.1 AA/AAA standards and suggest scrim.',
|
|
1660
|
+
parameters: {
|
|
1661
|
+
image: { type: 'string', description: 'Path or attachment ID of the image.' },
|
|
1662
|
+
colors: { type: 'array', items: { type: 'string' }, description: 'Optional list of background hex colors.' },
|
|
1663
|
+
text_color: { type: 'string', description: 'Text hex color to test against (default: #ffffff).' },
|
|
1664
|
+
},
|
|
1665
|
+
output: {
|
|
1666
|
+
schema: {
|
|
1667
|
+
type: 'object',
|
|
1668
|
+
additionalProperties: true,
|
|
1669
|
+
properties: {
|
|
1670
|
+
textHex: { type: 'string' },
|
|
1671
|
+
minContrastRatio: { type: 'number' },
|
|
1672
|
+
passedAA: { type: 'boolean' },
|
|
1673
|
+
passedAALarge: { type: 'boolean' },
|
|
1674
|
+
passedAAA: { type: 'boolean' },
|
|
1675
|
+
recommendation: { type: 'string' },
|
|
1676
|
+
suggestedScrimCss: { type: 'string' },
|
|
1677
|
+
details: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
async execute(args, exec) {
|
|
1682
|
+
let colors = args.colors
|
|
1683
|
+
if (!colors || colors.length === 0) {
|
|
1684
|
+
if (args.image) {
|
|
1685
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1686
|
+
if (source && source.bytes) {
|
|
1687
|
+
colors = extractSampleColorsFromBuffer(source.bytes)
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return checkWcagContrast(colors, args.text_color || '#ffffff')
|
|
1692
|
+
},
|
|
1693
|
+
}),
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
// #176: Интеллектуальная постобработка, очистка и оптимизация SVG
|
|
1697
|
+
ctx.tools.register(
|
|
1698
|
+
defineTool({
|
|
1699
|
+
name: 'optimize_vector_svg',
|
|
1700
|
+
description: 'Clean, sanitize, and minify SVG content, normalize viewBox, and export React TSX component.',
|
|
1701
|
+
parameters: {
|
|
1702
|
+
svg_content: { type: 'string', description: 'Raw SVG markup string to optimize.' },
|
|
1703
|
+
image: { type: 'string', description: 'Path or attachment ID of an SVG file if not passing raw string.' },
|
|
1704
|
+
component_name: { type: 'string', description: 'React component name for TSX export (default: VectorIcon).' },
|
|
1705
|
+
},
|
|
1706
|
+
output: {
|
|
1707
|
+
schema: {
|
|
1708
|
+
type: 'object',
|
|
1709
|
+
additionalProperties: true,
|
|
1710
|
+
properties: {
|
|
1711
|
+
svg: { type: 'string' },
|
|
1712
|
+
viewBox: { type: 'string' },
|
|
1713
|
+
originalSize: { type: 'number' },
|
|
1714
|
+
optimizedSize: { type: 'number' },
|
|
1715
|
+
savedBytes: { type: 'number' },
|
|
1716
|
+
reductionPercent: { type: 'number' },
|
|
1717
|
+
reactTsx: { type: 'string' },
|
|
1718
|
+
},
|
|
1719
|
+
},
|
|
1720
|
+
},
|
|
1721
|
+
async execute(args, exec) {
|
|
1722
|
+
let svgStr = args.svg_content
|
|
1723
|
+
if (!svgStr && args.image) {
|
|
1724
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1725
|
+
if (source && source.bytes) {
|
|
1726
|
+
svgStr = source.bytes.toString('utf8')
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (!svgStr) {
|
|
1730
|
+
throw new Error('Either svg_content or image must be provided')
|
|
1731
|
+
}
|
|
1732
|
+
return optimizeSvgContent(svgStr, { componentName: args.component_name })
|
|
1733
|
+
},
|
|
1734
|
+
}),
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
// #190: Генератор полного комплекта фавиконок, иконок приложений и PWA-манифеста
|
|
1738
|
+
ctx.tools.register(
|
|
1739
|
+
defineTool({
|
|
1740
|
+
name: 'generate_pwa_icon_suite',
|
|
1741
|
+
description: 'Generate standard PWA icons specification, HTML meta tags, and web app manifest.json.',
|
|
1742
|
+
parameters: {
|
|
1743
|
+
image: { type: 'string', description: 'Source icon image path or attachment ID.' },
|
|
1744
|
+
name: { type: 'string', description: 'App name (e.g. My Application).' },
|
|
1745
|
+
short_name: { type: 'string', description: 'App short name (e.g. MyApp).' },
|
|
1746
|
+
theme_color: { type: 'string', description: 'Theme color hex (e.g. #0f172a).' },
|
|
1747
|
+
background_color: { type: 'string', description: 'Background color hex (e.g. #ffffff).' },
|
|
1748
|
+
icons_dir: { type: 'string', description: 'Directory for icons relative to webroot (default: icons).' },
|
|
1749
|
+
},
|
|
1750
|
+
output: {
|
|
1751
|
+
schema: {
|
|
1752
|
+
type: 'object',
|
|
1753
|
+
additionalProperties: true,
|
|
1754
|
+
properties: {
|
|
1755
|
+
name: { type: 'string' },
|
|
1756
|
+
shortName: { type: 'string' },
|
|
1757
|
+
themeColor: { type: 'string' },
|
|
1758
|
+
backgroundColor: { type: 'string' },
|
|
1759
|
+
icons: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1760
|
+
manifestJson: { type: 'string' },
|
|
1761
|
+
htmlHeadSnippet: { type: 'string' },
|
|
1762
|
+
},
|
|
1763
|
+
},
|
|
1764
|
+
},
|
|
1765
|
+
async execute(args, exec) {
|
|
1766
|
+
return generatePwaIconSuite({
|
|
1767
|
+
name: args.name || 'App',
|
|
1768
|
+
shortName: args.short_name || args.name || 'App',
|
|
1769
|
+
themeColor: args.theme_color || '#0f172a',
|
|
1770
|
+
backgroundColor: args.background_color || '#ffffff',
|
|
1771
|
+
iconsDir: args.icons_dir || 'icons',
|
|
1772
|
+
sourcePath: args.image || null,
|
|
1773
|
+
})
|
|
1774
|
+
},
|
|
1775
|
+
}),
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1218
1778
|
})
|
|
1219
1779
|
|
|
1220
1780
|
}
|
package/lib/providers.js
CHANGED
|
@@ -353,13 +353,26 @@ export function makeProviders(deps, job) {
|
|
|
353
353
|
|
|
354
354
|
async function fal(seedArg = seed, promptArg = prompt) {
|
|
355
355
|
const key = await resolveKey(cfg.apiKeyEnv)
|
|
356
|
+
const isEdit = Boolean(source && source.bytes)
|
|
357
|
+
const targetModel = isEdit
|
|
358
|
+
? (mask && mask.bytes ? 'fal-ai/flux-pro/v1/inpaint' : 'fal-ai/flux/dev/image-to-image')
|
|
359
|
+
: cfg.model
|
|
356
360
|
const body = { prompt: promptArg, image_size: size, num_images: 1 }
|
|
357
361
|
if (seedArg !== undefined) body.seed = seedArg
|
|
358
362
|
if (negativePrompt !== undefined) body.negative_prompt = negativePrompt
|
|
359
363
|
if (guidanceScale !== undefined) body.guidance_scale = guidanceScale
|
|
360
364
|
if (format !== 'png') body.output_format = format
|
|
365
|
+
if (isEdit) {
|
|
366
|
+
body.image_url = `data:${source.mediaType || 'image/png'};base64,${Buffer.from(source.bytes).toString('base64')}`
|
|
367
|
+
if (mask && mask.bytes) {
|
|
368
|
+
body.mask_url = `data:${mask.mediaType || 'image/png'};base64,${Buffer.from(mask.bytes).toString('base64')}`
|
|
369
|
+
}
|
|
370
|
+
if (strength !== undefined) {
|
|
371
|
+
body.strength = strength
|
|
372
|
+
}
|
|
373
|
+
}
|
|
361
374
|
|
|
362
|
-
const submit = await submitJob(fetchImpl, cfg.baseURL,
|
|
375
|
+
const submit = await submitJob(fetchImpl, cfg.baseURL, targetModel, key, body, signal)
|
|
363
376
|
const statusUrl = submit.status_url || `${cfg.baseURL}/${cfg.model}/requests/${submit.request_id}/status`
|
|
364
377
|
const statusBody = await pollStatus(fetchImpl, statusUrl, key, signal, cfg.pollIntervalMs, cfg.timeoutMs)
|
|
365
378
|
|
|
@@ -517,6 +530,8 @@ export function makeProviders(deps, job) {
|
|
|
517
530
|
const kind = cfg.localKind === 'a1111' ? 'a1111' : 'comfyui'
|
|
518
531
|
|
|
519
532
|
if (kind === 'a1111') {
|
|
533
|
+
const isImg2Img = Boolean(source && source.bytes)
|
|
534
|
+
const endpoint = `${base}${isImg2Img ? '/sdapi/v1/img2img' : '/sdapi/v1/txt2img'}`
|
|
520
535
|
const body = {
|
|
521
536
|
prompt: promptArg,
|
|
522
537
|
negative_prompt: negativePrompt,
|
|
@@ -526,8 +541,15 @@ export function makeProviders(deps, job) {
|
|
|
526
541
|
cfg_scale: cfg.localCfg ?? 7,
|
|
527
542
|
seed: seedArg ?? -1,
|
|
528
543
|
}
|
|
544
|
+
if (isImg2Img) {
|
|
545
|
+
body.init_images = [Buffer.from(source.bytes).toString('base64')]
|
|
546
|
+
body.denoising_strength = strength ?? (mask ? 0.75 : 0.35)
|
|
547
|
+
if (mask && mask.bytes) {
|
|
548
|
+
body.mask = Buffer.from(mask.bytes).toString('base64')
|
|
549
|
+
}
|
|
550
|
+
}
|
|
529
551
|
if (cfg.localModel) body.override_settings = { sd_model_checkpoint: cfg.localModel }
|
|
530
|
-
const res = await fetchImpl(
|
|
552
|
+
const res = await fetchImpl(endpoint, {
|
|
531
553
|
method: 'POST',
|
|
532
554
|
headers: { 'Content-Type': 'application/json' },
|
|
533
555
|
body: JSON.stringify(body),
|
|
@@ -680,6 +702,11 @@ export function makeProviders(deps, job) {
|
|
|
680
702
|
aspect_ratio: aspectPixels ? `${aspectPixels[0]}:${aspectPixels[1]}` : (ASPECT_RATIOS[size] || '1:1'),
|
|
681
703
|
seed: seedArg,
|
|
682
704
|
...(negativePrompt ? { negative_prompt: negativePrompt } : {}),
|
|
705
|
+
...(source && source.bytes ? {
|
|
706
|
+
image: `data:${source.mediaType || 'image/png'};base64,${Buffer.from(source.bytes).toString('base64')}`,
|
|
707
|
+
...(mask && mask.bytes ? { mask: `data:${mask.mediaType || 'image/png'};base64,${Buffer.from(mask.bytes).toString('base64')}` } : {}),
|
|
708
|
+
...(strength !== undefined ? { prompt_strength: strength } : {}),
|
|
709
|
+
} : {}),
|
|
683
710
|
},
|
|
684
711
|
}
|
|
685
712
|
const res = await fetchImpl(`https://api.replicate.com/v1/models/${model}/predictions`, {
|
|
@@ -935,3 +962,24 @@ export async function blendImagesFal({ fetchImpl, resolveKey, cfg }, { images, w
|
|
|
935
962
|
const bytes = Buffer.from(await dl.arrayBuffer())
|
|
936
963
|
return { bytes, mediaType: 'image/png', width: img.width || 0, height: img.height || 0, sourceUrl: img.url }
|
|
937
964
|
}
|
|
965
|
+
|
|
966
|
+
/** Прямой вызов редактирования (inpainting / img2img) через выбранного провайдера. */
|
|
967
|
+
export async function editImageDirect(deps, job) {
|
|
968
|
+
const providers = makeProviders(deps, job)
|
|
969
|
+
const providerKey = job.provider || deps.cfg.defaultProvider || 'fal'
|
|
970
|
+
const fn = providers[providerKey] || providers.fal || providers.custom
|
|
971
|
+
if (!fn) throw new Error(`Provider "${providerKey}" does not support image editing`)
|
|
972
|
+
return fn(job.seed, job.prompt)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Прямой вызов вариаций изображения через выбранного провайдера. */
|
|
976
|
+
export async function varyImageDirect(deps, job) {
|
|
977
|
+
const providers = makeProviders(deps, {
|
|
978
|
+
...job,
|
|
979
|
+
strength: job.variationStrength ?? job.strength ?? 0.35,
|
|
980
|
+
})
|
|
981
|
+
const providerKey = job.provider || deps.cfg.defaultProvider || 'fal'
|
|
982
|
+
const fn = providers[providerKey] || providers.fal || providers.custom
|
|
983
|
+
if (!fn) throw new Error(`Provider "${providerKey}" does not support image variations`)
|
|
984
|
+
return fn(job.seed, job.prompt)
|
|
985
|
+
}
|