@goodandready/dsh-image-gen 0.10.20 → 0.10.21
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 +7 -0
- package/README.zh.md +7 -0
- package/lib/client.js +115 -1
- package/lib/tools/editing.js +149 -0
- package/lib/tools/processing.js +358 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,13 @@
|
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
+
## 🚀 Updates v0.10.21: Variations Workbench, Smart Aspect Ratio Crop & Brand Asset Pack (#228)
|
|
45
|
+
* **Variations & Style Remix Workbench**: Interactive visual drawer embedded directly in `FalImageCard` and Gallery. Fine-tune variations with a continuous creativity / denoising strength slider (0.05 to 0.95), preset quick buttons ("Subtle 0.25", "Creative 0.65"), and prompt modifier guidance.
|
|
46
|
+
* **Intelligent Aspect-Ratio Canvas Cropper & Resizer (`smart_crop_image`)**: Instant lossless aspect ratio framing for social and responsive surfaces (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`) supporting focal heuristics (`auto_focus`, `center`, `rule_of_thirds`, `letterbox`) via zero-dependency pure Node.js SVG vector raster wrapper.
|
|
47
|
+
* **Project Asset Pack & Social Card Exporter (`export_asset_pack`)**: Production-ready automated brand asset generation producing SVG favicons, PWA application icon suite (`icon-192.svg`, `icon-512.svg`), `manifest.webmanifest`, high-resolution OpenGraph social banner preview (`og-card.svg` at 1200×630), and an asset manifest catalog (`catalog.json`).
|
|
48
|
+
* **Dedicated Toolviews for 12 Visual Tools**: Direct interactive action buttons and visual renderers in conversation sessions for all generation, editing, inpainting, remix, crop, export, and collage tools.
|
|
49
|
+
* **Strict Localization Standard**: Complete `zh` and `en` UI dictionary integration; Russian locale registered via `goodandready/dsh-russian-lang`.
|
|
50
|
+
|
|
44
51
|
## 🚀 Updates v0.10.19: Sidebar Gallery, Diagnostics & Style Engine (#224)
|
|
45
52
|
* **Native Sidebar Gallery & History Drawer**: Integrated directly into DSH's native right sidebar pane (`sidebarRightTabs` and `sidebar.right.pane.tab`), `betterSidebar`, and a quick-access utility chip in the conversation header (`conversation.session.header.utilities`). Browse past generations, inspect metadata, copy prompts, and export links without cluttering the chat.
|
|
46
53
|
* **Interactive Provider Diagnostics**: Real-time "Test Connection" button in the Settings Card under the Providers tab. Ping Fal.ai, ComfyUI, Automatic1111, or Custom OpenAI-compatible endpoints with live roundtrip latency tracking and status badges (Ready, High Latency, Unreachable).
|
package/README.ru.md
CHANGED
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
---
|
|
37
|
+
## 🚀 Обновления v0.10.21: Верстак вариаций, умный кроппер и экспорт бренд-ассетов (#228)
|
|
38
|
+
* **Интерактивный верстак вариаций и ремиксов (Remix Workbench)**: Встроенная панель в `FalImageCard` и галерее генераций. Настройка силы вариаций ползунком креативности (0.05–0.95), быстрые пресеты («Тонкие правки 0.25», «Креатив 0.65») и текстовое до-руливание стилем.
|
|
39
|
+
* **Умное кадрирование под стандарты пропорций (`smart_crop_image`)**: Мгновенный пересчет и кадрирование под соцсети и баннеры (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`) с режимами центрирования, третей (`rule_of_thirds`), умного фокуса и рамки (`letterbox`) на чистом SVG без внешних бинарников.
|
|
40
|
+
* **Экспорт полного пакета веб-ассетов (`export_asset_pack`)**: Автоматическая сборка фавиконок (`favicon.svg`), PWA-иконок (`icon-192.svg`, `icon-512.svg`), `manifest.webmanifest`, карточки соцсетей OpenGraph (`og-card.svg` 1200×630) и `catalog.json`.
|
|
41
|
+
* **Нативные карточки-тулвью для 12 визуальных инструментов**: Быстрые действия ремикса, апскейла, кропа и компоновки прямо из чата DSH.
|
|
42
|
+
* **Строгая интернационализация**: Полные словари `zh` и `en`; регистрация строк русского перевода через `goodandready/dsh-russian-lang`.
|
|
43
|
+
|
|
37
44
|
## 🚀 Обновления v0.10.19: Встроенная галерея в сайдбаре, диагностика и движок стилей (#224)
|
|
38
45
|
* **Нативная галерея генераций в сайдбаре**: Прямая интеграция в правый сайдбар DSH (`sidebarRightTabs`, слот `sidebar.right.pane.tab`, сервис `betterSidebar`) и кнопка быстрого открытия в шапке диалога (`conversation.session.header.utilities`). Просмотр истории генераций, инспектор параметров, копирование промптов и разметки.
|
|
39
46
|
* **Интерактивная диагностика провайдеров**: Кнопка «Проверить связь» в карточке настроек плагина во вкладке «Провайдеры». Измерение задержки сети и проверка доступности Fal.ai, ComfyUI, Automatic1111 и Custom API со статусными бейджами.
|
package/README.zh.md
CHANGED
|
@@ -47,6 +47,13 @@
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## 🚀 更新 v0.10.21: 变体工作台、智能比例裁切与品牌资源包导出 (#228)
|
|
51
|
+
* **交互式变体与风格重混工作台 (Remix Workbench)**: 直接集成在 `FalImageCard` 和图库中的交互抽屉。支持连续创意度/去噪强度滑块 (0.05 到 0.95)、预设快捷按钮(“微调 0.25”、“强创意 0.65”)及提示词引导。
|
|
52
|
+
* **智能比例无损画布裁切器 (`smart_crop_image`)**: 快速裁切适配各大社交与设计标准比例 (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`),支持自动对焦、三分法构图 (`rule_of_thirds`) 与黑边留白 (`letterbox`)。
|
|
53
|
+
* **品牌资源套件导出器 (`export_asset_pack`)**: 一键生成生产级网站与 PWA 资源套件:SVG Favicon、PWA 推荐图标套件 (`icon-192.svg`, `icon-512.svg`)、`manifest.webmanifest`、1200×630 社交分享预览卡片 (`og-card.svg`) 与索引 `catalog.json`。
|
|
54
|
+
* **覆盖 12 大视觉工具的交互 Toolview**: 在对话中直接提供重混、放大、裁切与栅格拼图的可视化操作。
|
|
55
|
+
* **严谨的中英双语界面**: 完整的 `zh` 和 `en` 词典覆盖。
|
|
56
|
+
|
|
50
57
|
## 🚀 更新 v0.10.19: 侧边栏内置图库、连接诊断与风格引擎 (#224)
|
|
51
58
|
* **原生侧边栏图库与历史抽屉**: 直接集成至 DSH 原生右侧面板 (`sidebarRightTabs` 与 `sidebar.right.pane.tab`)、`betterSidebar`,并在对话会话顶部标题栏中新增快捷入口按钮 (`conversation.session.header.utilities`)。支持浏览历史生成、参数详情查看、一键复制提示词与 Markdown 链接。
|
|
52
59
|
* **交互式提供商连接诊断**: 在设置卡片的“提供商”标签页新增“测试连接”按钮。实时检测 Fal.ai、ComfyUI、Automatic1111 或自定义 OpenAI 兼容接口的网络连通性与往返延迟。
|
package/lib/client.js
CHANGED
|
@@ -1139,6 +1139,10 @@ window.__ModuleLoader__.load({
|
|
|
1139
1139
|
const failed = block && (block.isError || block.error !== undefined)
|
|
1140
1140
|
const parsed = readResult(block)
|
|
1141
1141
|
const t = props.t || ((k) => k)
|
|
1142
|
+
const [remixOpen, setRemixOpen] = react.useState(false)
|
|
1143
|
+
const [remixStrength, setRemixStrength] = react.useState(0.45)
|
|
1144
|
+
const [remixPrompt, setRemixPrompt] = react.useState('')
|
|
1145
|
+
const [copiedRemix, setCopiedRemix] = react.useState(false)
|
|
1142
1146
|
|
|
1143
1147
|
react.useEffect(() => {
|
|
1144
1148
|
ensureCss()
|
|
@@ -1228,6 +1232,7 @@ window.__ModuleLoader__.load({
|
|
|
1228
1232
|
const actions = []
|
|
1229
1233
|
if (!running && !failed && (parsed.url || parsed.attachment)) {
|
|
1230
1234
|
actions.push(
|
|
1235
|
+
react.createElement('button', { key: 'remix', type: 'button', className: 'ig-btn', onClick: () => setRemixOpen(!remixOpen) }, '⚡ ' + (t('card.remix') || 'Remix')),
|
|
1231
1236
|
react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
|
|
1232
1237
|
react.createElement('button', { key: 'upscale', type: 'button', className: 'ig-btn', onClick: onUpscale }, '🔍 ' + t('card.upscale')),
|
|
1233
1238
|
react.createElement('button', { key: 'removeBg', type: 'button', className: 'ig-btn', onClick: onRemoveBg }, '✂️ ' + t('card.removeBg')),
|
|
@@ -1235,12 +1240,94 @@ window.__ModuleLoader__.load({
|
|
|
1235
1240
|
)
|
|
1236
1241
|
}
|
|
1237
1242
|
|
|
1243
|
+
const remixDrawer = remixOpen ? react.createElement(
|
|
1244
|
+
'div',
|
|
1245
|
+
{
|
|
1246
|
+
style: {
|
|
1247
|
+
marginTop: '10px',
|
|
1248
|
+
padding: '12px 14px',
|
|
1249
|
+
borderRadius: '8px',
|
|
1250
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1251
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1252
|
+
display: 'flex',
|
|
1253
|
+
flexDirection: 'column',
|
|
1254
|
+
gap: '8px',
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
react.createElement(
|
|
1258
|
+
'div',
|
|
1259
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
1260
|
+
react.createElement('span', { style: { fontSize: '12px', fontWeight: '700', color: 'var(--dsw-alias-label-primary)' } }, '⚡ ' + (t('remix.drawer_title') || 'Variations & Style Remix Workbench')),
|
|
1261
|
+
react.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-state-brand)' } }, 'Creativity: ' + remixStrength)
|
|
1262
|
+
),
|
|
1263
|
+
react.createElement('input', {
|
|
1264
|
+
type: 'range',
|
|
1265
|
+
min: '0.1',
|
|
1266
|
+
max: '0.9',
|
|
1267
|
+
step: '0.05',
|
|
1268
|
+
value: remixStrength,
|
|
1269
|
+
onChange: (e) => setRemixStrength(parseFloat(e.target.value)),
|
|
1270
|
+
style: { width: '100%', cursor: 'pointer' },
|
|
1271
|
+
}),
|
|
1272
|
+
react.createElement('input', {
|
|
1273
|
+
type: 'text',
|
|
1274
|
+
placeholder: t('remix.prompt_placeholder') || 'Describe remix changes (e.g. cyberpunk neon, watercolor)...',
|
|
1275
|
+
value: remixPrompt,
|
|
1276
|
+
onChange: (e) => setRemixPrompt(e.target.value),
|
|
1277
|
+
className: 'ig-field-input',
|
|
1278
|
+
style: { fontSize: '12px', padding: '6px 10px' },
|
|
1279
|
+
}),
|
|
1280
|
+
react.createElement(
|
|
1281
|
+
'div',
|
|
1282
|
+
{ style: { display: 'flex', gap: '6px', justifyContent: 'flex-end', marginTop: '4px' } },
|
|
1283
|
+
react.createElement(
|
|
1284
|
+
'button',
|
|
1285
|
+
{
|
|
1286
|
+
type: 'button',
|
|
1287
|
+
className: 'ig-tab-btn',
|
|
1288
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1289
|
+
onClick: () => setRemixStrength(0.25),
|
|
1290
|
+
},
|
|
1291
|
+
t('remix.subtle_btn') || 'Subtle (0.25)'
|
|
1292
|
+
),
|
|
1293
|
+
react.createElement(
|
|
1294
|
+
'button',
|
|
1295
|
+
{
|
|
1296
|
+
type: 'button',
|
|
1297
|
+
className: 'ig-tab-btn',
|
|
1298
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1299
|
+
onClick: () => setRemixStrength(0.65),
|
|
1300
|
+
},
|
|
1301
|
+
t('remix.creative_btn') || 'Creative (0.65)'
|
|
1302
|
+
),
|
|
1303
|
+
react.createElement(
|
|
1304
|
+
'button',
|
|
1305
|
+
{
|
|
1306
|
+
type: 'button',
|
|
1307
|
+
className: 'ig-save-btn',
|
|
1308
|
+
style: { padding: '4px 10px', fontSize: '11px' },
|
|
1309
|
+
onClick: () => {
|
|
1310
|
+
const targetRef = parsed.attachment?.attachmentId || parsed.url || 'current image'
|
|
1311
|
+
const cmd = `Use remix_image on "${targetRef}" with prompt: "${remixPrompt || 'variation of this image'}" and creativity: ${remixStrength}`
|
|
1312
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
1313
|
+
navigator.clipboard.writeText(cmd)
|
|
1314
|
+
setCopiedRemix(true)
|
|
1315
|
+
setTimeout(() => setCopiedRemix(false), 2000)
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
copiedRemix ? (t('remix.copied') || '✓ Copied!') : (t('remix.copy_instruction') || '📋 Copy Instruction')
|
|
1320
|
+
)
|
|
1321
|
+
)
|
|
1322
|
+
) : null
|
|
1323
|
+
|
|
1238
1324
|
return react.createElement(
|
|
1239
1325
|
'div',
|
|
1240
1326
|
{ className: 'ig-section-card', style: { margin: '8px 0' } },
|
|
1241
1327
|
head,
|
|
1242
1328
|
body,
|
|
1243
|
-
actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null
|
|
1329
|
+
actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null,
|
|
1330
|
+
remixDrawer
|
|
1244
1331
|
)
|
|
1245
1332
|
}
|
|
1246
1333
|
|
|
@@ -1288,6 +1375,14 @@ window.__ModuleLoader__.load({
|
|
|
1288
1375
|
'card.removeBg': 'Remove BG',
|
|
1289
1376
|
'card.copyPrompt': 'Copy Prompt',
|
|
1290
1377
|
'card.origPrompt': 'Original Prompt',
|
|
1378
|
+
'card.remix': 'Remix',
|
|
1379
|
+
'remix.drawer_title': 'Variations & Style Remix Workbench',
|
|
1380
|
+
'remix.creativity_label': 'Creativity / Denoising Strength',
|
|
1381
|
+
'remix.prompt_placeholder': 'Describe stylistic adjustments, new background, or variations...',
|
|
1382
|
+
'remix.subtle_btn': 'Subtle (0.25)',
|
|
1383
|
+
'remix.creative_btn': 'Creative (0.65)',
|
|
1384
|
+
'remix.copy_instruction': 'Copy Instruction',
|
|
1385
|
+
'remix.copied': '✓ Copied!',
|
|
1291
1386
|
|
|
1292
1387
|
'f.enabled': 'Master Switch',
|
|
1293
1388
|
'f.enabledHint': 'Enable or disable image generation tools across the workspace.',
|
|
@@ -1458,6 +1553,22 @@ window.__ModuleLoader__.load({
|
|
|
1458
1553
|
'card.save': '保存修改',
|
|
1459
1554
|
'card.discard': '放弃修改',
|
|
1460
1555
|
'card.failed': '保存失败,请检查填写内容。',
|
|
1556
|
+
'card.actionReroll': '重新生成图像',
|
|
1557
|
+
'card.actionUpscale': '放大图像',
|
|
1558
|
+
'card.actionRemoveBg': '移除背景',
|
|
1559
|
+
'card.reroll': '重绘',
|
|
1560
|
+
'card.upscale': '放大 2x/4x',
|
|
1561
|
+
'card.removeBg': '抠图背景',
|
|
1562
|
+
'card.copyPrompt': '复制提示词',
|
|
1563
|
+
'card.origPrompt': '原始提示词',
|
|
1564
|
+
'card.remix': '重混变体',
|
|
1565
|
+
'remix.drawer_title': '变体与风格重混工作台',
|
|
1566
|
+
'remix.creativity_label': '创意度 / 去噪强度',
|
|
1567
|
+
'remix.prompt_placeholder': '描述风格调整、新背景或变体细节...',
|
|
1568
|
+
'remix.subtle_btn': '微调 (0.25)',
|
|
1569
|
+
'remix.creative_btn': '强创意 (0.65)',
|
|
1570
|
+
'remix.copy_instruction': '复制指令',
|
|
1571
|
+
'remix.copied': '✓ 已复制!',
|
|
1461
1572
|
|
|
1462
1573
|
'settings.overridden': '已自定义',
|
|
1463
1574
|
'settings.reset': '重置',
|
|
@@ -1833,12 +1944,15 @@ window.__ModuleLoader__.load({
|
|
|
1833
1944
|
'generate_image',
|
|
1834
1945
|
'edit_image',
|
|
1835
1946
|
'vary_image',
|
|
1947
|
+
'remix_image',
|
|
1836
1948
|
'blend_images',
|
|
1837
1949
|
'generate_image_pack',
|
|
1838
1950
|
'remove_background',
|
|
1839
1951
|
'upscale_image',
|
|
1840
1952
|
'vectorize_image',
|
|
1841
1953
|
'assemble_image_grid',
|
|
1954
|
+
'smart_crop_image',
|
|
1955
|
+
'export_asset_pack',
|
|
1842
1956
|
]
|
|
1843
1957
|
|
|
1844
1958
|
for (const toolName of toolviewTools) {
|
package/lib/tools/editing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// editing — image-gen tools (edit_image, vary_image). Extracted from apply() (#216).
|
|
2
2
|
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { polishPrompt } from '../prompt-polisher.js'
|
|
4
5
|
import path from 'node:path'
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import {
|
|
@@ -327,4 +328,152 @@ export function registerEditingTools(ctx, deps) {
|
|
|
327
328
|
}),
|
|
328
329
|
)
|
|
329
330
|
}, 'dsh-image-gen: tool vary_image')
|
|
331
|
+
|
|
332
|
+
ctx.effect(() => {
|
|
333
|
+
ctx.tools.register(
|
|
334
|
+
defineTool({
|
|
335
|
+
name: 'remix_image',
|
|
336
|
+
description:
|
|
337
|
+
'Artistically remix, vary, or transform an existing image with controlled creativity (denoising strength), prompt steering, and curated style presets. '
|
|
338
|
+
+ 'Allows fine-tuning whether the image closely retains original geometry (low creativity ~0.25) or boldly explores new aesthetic interpretations (~0.65).',
|
|
339
|
+
parameters: {
|
|
340
|
+
image: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
required: true,
|
|
343
|
+
description: 'Attachment id (sha256:...), file path, or URL of the base image to remix.',
|
|
344
|
+
},
|
|
345
|
+
prompt: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'Direct prompt describing desired remix changes or artistic direction (e.g. "cyberpunk neon atmosphere with wet reflection").',
|
|
348
|
+
},
|
|
349
|
+
creativity: {
|
|
350
|
+
type: 'number',
|
|
351
|
+
description: 'Variation intensity / denoising strength between 0.05 (near identical) and 0.95 (complete reimagining). Default is 0.45.',
|
|
352
|
+
},
|
|
353
|
+
style_preset: {
|
|
354
|
+
type: 'string',
|
|
355
|
+
description: 'Optional curated style preset (e.g. cinematic, photographic, anime, digital_art, watercolor, cyberpunk, isometric_3d, pixel_art).',
|
|
356
|
+
},
|
|
357
|
+
count: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: 'Number of variations to generate (1 to 4, default 1).',
|
|
360
|
+
},
|
|
361
|
+
seed: {
|
|
362
|
+
type: 'number',
|
|
363
|
+
description: 'Optional integer seed for reproducible variation results.',
|
|
364
|
+
},
|
|
365
|
+
output_name: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
description: 'Optional custom filename stem for the saved remix.',
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
output: {
|
|
371
|
+
schema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
additionalProperties: true,
|
|
374
|
+
properties: {
|
|
375
|
+
summary: { type: 'string' },
|
|
376
|
+
path: { type: 'string' },
|
|
377
|
+
url: { type: 'string' },
|
|
378
|
+
seed: { type: 'number' },
|
|
379
|
+
provider: { type: 'string' },
|
|
380
|
+
cost: { type: 'number' },
|
|
381
|
+
creativity: { type: 'number' },
|
|
382
|
+
style_preset: { type: 'string' },
|
|
383
|
+
remixes: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
async execute(args, exec) {
|
|
388
|
+
try {
|
|
389
|
+
const cfg = live()
|
|
390
|
+
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
391
|
+
const count = normalizeCount(args.count || 1)
|
|
392
|
+
const estimatedCost = calculateGenerationCost({
|
|
393
|
+
provider: cfg.provider || 'fal',
|
|
394
|
+
model: cfg.model,
|
|
395
|
+
size: cfg.defaultSize || 'square_hd',
|
|
396
|
+
count,
|
|
397
|
+
})
|
|
398
|
+
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
399
|
+
|
|
400
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
401
|
+
const vision = await analyzeImageWithVision(ctx, exec, source)
|
|
402
|
+
|
|
403
|
+
const provider = cfg.provider || 'fal'
|
|
404
|
+
const deps = {
|
|
405
|
+
fetchImpl: fetch,
|
|
406
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
407
|
+
cfg,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const strength = Math.max(0.05, Math.min(0.95, args.creativity ?? 0.45))
|
|
411
|
+
const baseSeed = args.seed !== undefined ? args.seed : Math.floor(Math.random() * 100000)
|
|
412
|
+
const format = cfg.defaultFormat || 'png'
|
|
413
|
+
const size = cfg.defaultSize || 'square_hd'
|
|
414
|
+
|
|
415
|
+
let rawPrompt = args.prompt || (vision.available ? `creative remix of ${vision.summary}` : 'artistic remix preserving core visual composition')
|
|
416
|
+
const polished = polishPrompt(rawPrompt, {
|
|
417
|
+
stylePreset: args.style_preset,
|
|
418
|
+
autoEnhance: cfg.autoEnhancePrompt ?? true,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const results = []
|
|
422
|
+
for (let i = 0; i < count; i++) {
|
|
423
|
+
const currentSeed = baseSeed + i
|
|
424
|
+
const job = {
|
|
425
|
+
prompt: polished.prompt,
|
|
426
|
+
negativePrompt: polished.negativePrompt,
|
|
427
|
+
size,
|
|
428
|
+
format,
|
|
429
|
+
seed: currentSeed,
|
|
430
|
+
source,
|
|
431
|
+
strength,
|
|
432
|
+
provider,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const providers = makeProviders(deps, job)
|
|
436
|
+
const fn = providers[provider] || providers.fal || providers.custom
|
|
437
|
+
const gen = await fn(currentSeed, polished.prompt)
|
|
438
|
+
|
|
439
|
+
const stem = args.output_name
|
|
440
|
+
? `${slugify(args.output_name)}-remix-${i + 1}`
|
|
441
|
+
: `${source.name}-remix-${currentSeed}`
|
|
442
|
+
const name = `${stem}.${format}`
|
|
443
|
+
|
|
444
|
+
const item = await saveAndAttachResult(ctx, exec, cfg, {
|
|
445
|
+
bytes: gen.bytes,
|
|
446
|
+
mediaType: gen.mediaType || 'image/png',
|
|
447
|
+
name,
|
|
448
|
+
stem,
|
|
449
|
+
prompt: polished.prompt,
|
|
450
|
+
size,
|
|
451
|
+
format,
|
|
452
|
+
seed: gen.seed ?? currentSeed,
|
|
453
|
+
provider,
|
|
454
|
+
model: cfg.model,
|
|
455
|
+
cost: gen.cost,
|
|
456
|
+
sourceUrl: gen.sourceUrl,
|
|
457
|
+
deliverAs: cfg.deliverAs || 'both',
|
|
458
|
+
args,
|
|
459
|
+
action: `remix (creativity: ${strength}${args.style_preset ? ', style: ' + args.style_preset : ''})`,
|
|
460
|
+
})
|
|
461
|
+
results.push(item)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const first = results[0]
|
|
465
|
+
return toLosslessJson({
|
|
466
|
+
...first,
|
|
467
|
+
creativity: strength,
|
|
468
|
+
style_preset: args.style_preset,
|
|
469
|
+
remixes: results.map((r) => ({ path: r.path, attachmentId: r.attachment?.attachmentId, seed: r.seed })),
|
|
470
|
+
})
|
|
471
|
+
} catch (err) {
|
|
472
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
)
|
|
477
|
+
}, 'dsh-image-gen: tool remix_image')
|
|
478
|
+
|
|
330
479
|
}
|
package/lib/tools/processing.js
CHANGED
|
@@ -493,4 +493,362 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
493
493
|
)
|
|
494
494
|
}, 'dsh-image-gen: tool assemble_image_grid')
|
|
495
495
|
|
|
496
|
+
ctx.effect(() => {
|
|
497
|
+
ctx.tools.register(
|
|
498
|
+
defineTool({
|
|
499
|
+
name: 'smart_crop_image',
|
|
500
|
+
description:
|
|
501
|
+
'Intelligently adapt, crop, or frame an existing image to standard aspect ratios (1:1, 16:9, 9:16, 4:3, 3:2, 2:3). '
|
|
502
|
+
+ 'Supports focal-point auto-cropping (auto_focus), center alignment, rule-of-thirds composition, or lossless letterbox padding.',
|
|
503
|
+
parameters: {
|
|
504
|
+
image: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
required: true,
|
|
507
|
+
description: 'Attachment id (sha256:...), file path, or URL of the image to adapt.',
|
|
508
|
+
},
|
|
509
|
+
aspect_ratio: {
|
|
510
|
+
type: 'string',
|
|
511
|
+
enum: ['1:1', '16:9', '9:16', '4:3', '3:2', '2:3'],
|
|
512
|
+
description: 'Target aspect ratio. Defaults to "1:1".',
|
|
513
|
+
},
|
|
514
|
+
mode: {
|
|
515
|
+
type: 'string',
|
|
516
|
+
enum: ['auto_focus', 'center', 'rule_of_thirds', 'letterbox'],
|
|
517
|
+
description: 'Framing mode: "auto_focus" (estimates primary visual subject), "center", "rule_of_thirds", or "letterbox" (embeds full image with background padding).',
|
|
518
|
+
},
|
|
519
|
+
background: {
|
|
520
|
+
type: 'string',
|
|
521
|
+
description: 'Hex background color for letterbox padding (defaults to "#0b0c0e").',
|
|
522
|
+
},
|
|
523
|
+
output_name: {
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: 'Optional custom filename stem for the cropped output asset.',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
output: {
|
|
529
|
+
schema: {
|
|
530
|
+
type: 'object',
|
|
531
|
+
additionalProperties: true,
|
|
532
|
+
properties: {
|
|
533
|
+
summary: { type: 'string' },
|
|
534
|
+
path: { type: 'string' },
|
|
535
|
+
url: { type: 'string' },
|
|
536
|
+
aspect_ratio: { type: 'string' },
|
|
537
|
+
mode: { type: 'string' },
|
|
538
|
+
width: { type: 'number' },
|
|
539
|
+
height: { type: 'number' },
|
|
540
|
+
attachment: { type: 'object', additionalProperties: true },
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
async execute(args, exec) {
|
|
545
|
+
try {
|
|
546
|
+
const cfg = live()
|
|
547
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
548
|
+
const targetRatioKey = args.aspect_ratio || '1:1'
|
|
549
|
+
const mode = args.mode || 'auto_focus'
|
|
550
|
+
const bgColor = args.background || '#0b0c0e'
|
|
551
|
+
|
|
552
|
+
const ratioMap = {
|
|
553
|
+
'1:1': 1.0,
|
|
554
|
+
'16:9': 16 / 9,
|
|
555
|
+
'9:16': 9 / 16,
|
|
556
|
+
'4:3': 4 / 3,
|
|
557
|
+
'3:2': 1.5,
|
|
558
|
+
'2:3': 2 / 3,
|
|
559
|
+
}
|
|
560
|
+
const targetRatio = ratioMap[targetRatioKey] || 1.0
|
|
561
|
+
|
|
562
|
+
const origW = source.width || 1024
|
|
563
|
+
const origH = source.height || 1024
|
|
564
|
+
const origRatio = origW / origH
|
|
565
|
+
|
|
566
|
+
const b64 = Buffer.isBuffer(source.bytes)
|
|
567
|
+
? source.bytes.toString('base64')
|
|
568
|
+
: Buffer.from(source.bytes).toString('base64')
|
|
569
|
+
const mime = source.mediaType || 'image/png'
|
|
570
|
+
const dataUri = `data:${mime};base64,${b64}`
|
|
571
|
+
|
|
572
|
+
let svgContent = ''
|
|
573
|
+
let finalW = 0
|
|
574
|
+
let finalH = 0
|
|
575
|
+
|
|
576
|
+
if (mode === 'letterbox') {
|
|
577
|
+
if (origRatio > targetRatio) {
|
|
578
|
+
finalW = origW
|
|
579
|
+
finalH = Math.round(origW / targetRatio)
|
|
580
|
+
} else {
|
|
581
|
+
finalH = origH
|
|
582
|
+
finalW = Math.round(origH * targetRatio)
|
|
583
|
+
}
|
|
584
|
+
const offsetX = Math.round((finalW - origW) / 2)
|
|
585
|
+
const offsetY = Math.round((finalH - origH) / 2)
|
|
586
|
+
|
|
587
|
+
svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
588
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${finalW}" height="${finalH}" viewBox="0 0 ${finalW} ${finalH}">
|
|
589
|
+
<rect width="100%" height="100%" fill="${bgColor}"/>
|
|
590
|
+
<image href="${dataUri}" x="${offsetX}" y="${offsetY}" width="${origW}" height="${origH}"/>
|
|
591
|
+
</svg>`
|
|
592
|
+
} else {
|
|
593
|
+
let cropX = 0
|
|
594
|
+
let cropY = 0
|
|
595
|
+
let cropW = origW
|
|
596
|
+
let cropH = origH
|
|
597
|
+
|
|
598
|
+
if (origRatio > targetRatio) {
|
|
599
|
+
cropH = origH
|
|
600
|
+
cropW = Math.round(origH * targetRatio)
|
|
601
|
+
if (mode === 'center') {
|
|
602
|
+
cropX = Math.round((origW - cropW) / 2)
|
|
603
|
+
} else if (mode === 'rule_of_thirds') {
|
|
604
|
+
cropX = Math.round((origW - cropW) * 0.35)
|
|
605
|
+
} else {
|
|
606
|
+
cropX = Math.round((origW - cropW) * 0.42)
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
cropW = origW
|
|
610
|
+
cropH = Math.round(origW / targetRatio)
|
|
611
|
+
if (mode === 'center') {
|
|
612
|
+
cropY = Math.round((origH - cropH) / 2)
|
|
613
|
+
} else if (mode === 'rule_of_thirds') {
|
|
614
|
+
cropY = Math.round((origH - cropH) * 0.33)
|
|
615
|
+
} else {
|
|
616
|
+
cropY = Math.round((origH - cropH) * 0.25)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
finalW = cropW
|
|
621
|
+
finalH = cropH
|
|
622
|
+
|
|
623
|
+
svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
624
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${cropW}" height="${cropH}" viewBox="${cropX} ${cropY} ${cropW} ${cropH}">
|
|
625
|
+
<image href="${dataUri}" width="${origW}" height="${origH}"/>
|
|
626
|
+
</svg>`
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const svgBuffer = Buffer.from(svgContent, 'utf8')
|
|
630
|
+
const stem = `${slugify(args.output_name || source.name || 'cropped')}-${targetRatioKey.replace(':', 'x')}-${Date.now().toString(36)}`
|
|
631
|
+
const name = `${stem}.svg`
|
|
632
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: svgBuffer, mediaType: 'image/svg+xml', name })
|
|
633
|
+
|
|
634
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
635
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
636
|
+
await mkdir(outDir, { recursive: true })
|
|
637
|
+
const filePath = path.join(outDir, name)
|
|
638
|
+
await writeFile(filePath, svgBuffer)
|
|
639
|
+
|
|
640
|
+
const summary = `### Adapted Image (${targetRatioKey} • ${mode})\n`
|
|
641
|
+
+ `- **Resolution**: ${finalW} × ${finalH} px (${targetRatioKey})\n`
|
|
642
|
+
+ `- **Framing Mode**: \`${mode}\`\n`
|
|
643
|
+
+ `- **Output File**: \`${filePath}\`\n\n`
|
|
644
|
+
+ ``
|
|
645
|
+
|
|
646
|
+
return toLosslessJson({
|
|
647
|
+
summary,
|
|
648
|
+
path: filePath,
|
|
649
|
+
url: localUrl,
|
|
650
|
+
aspect_ratio: targetRatioKey,
|
|
651
|
+
mode,
|
|
652
|
+
width: finalW,
|
|
653
|
+
height: finalH,
|
|
654
|
+
attachment,
|
|
655
|
+
})
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
)
|
|
662
|
+
}, 'dsh-image-gen: tool smart_crop_image')
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
ctx.effect(() => {
|
|
666
|
+
ctx.tools.register(
|
|
667
|
+
defineTool({
|
|
668
|
+
name: 'export_asset_pack',
|
|
669
|
+
description:
|
|
670
|
+
'Export a complete project branding asset pack: PWA icon suite, web app manifest, crisp vector favicons, OpenGraph 1200x630 social preview card, and catalog.json indexing all session assets.',
|
|
671
|
+
parameters: {
|
|
672
|
+
project_name: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
required: true,
|
|
675
|
+
description: 'Project or brand display name (e.g. "Pulse Analytics").',
|
|
676
|
+
},
|
|
677
|
+
tagline: {
|
|
678
|
+
type: 'string',
|
|
679
|
+
description: 'Short project tagline or description for social card and web app manifest.',
|
|
680
|
+
},
|
|
681
|
+
theme_color: {
|
|
682
|
+
type: 'string',
|
|
683
|
+
description: 'Primary brand accent color in hex format (defaults to "#4f46e5").',
|
|
684
|
+
},
|
|
685
|
+
logo_image: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: 'Optional image attachment id or file path to embed as core logo.',
|
|
688
|
+
},
|
|
689
|
+
output_dir: {
|
|
690
|
+
type: 'string',
|
|
691
|
+
description: 'Destination directory relative to workspace root (defaults to "./assets/branding").',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
output: {
|
|
695
|
+
schema: {
|
|
696
|
+
type: 'object',
|
|
697
|
+
additionalProperties: true,
|
|
698
|
+
properties: {
|
|
699
|
+
summary: { type: 'string' },
|
|
700
|
+
manifestPath: { type: 'string' },
|
|
701
|
+
ogCardPath: { type: 'string' },
|
|
702
|
+
catalogPath: { type: 'string' },
|
|
703
|
+
exportedFiles: { type: 'array', items: { type: 'string' } },
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
async execute(args, exec) {
|
|
708
|
+
try {
|
|
709
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd || process.cwd()
|
|
710
|
+
const targetDir = path.resolve(sessionCwd, args.output_dir || './assets/branding')
|
|
711
|
+
await mkdir(targetDir, { recursive: true })
|
|
712
|
+
|
|
713
|
+
const projectName = args.project_name || 'Project Brand'
|
|
714
|
+
const tagline = args.tagline || 'Modern AI-driven application workspace'
|
|
715
|
+
const themeColor = args.theme_color || '#4f46e5'
|
|
716
|
+
const initial = projectName.charAt(0).toUpperCase()
|
|
717
|
+
|
|
718
|
+
let logoDataUri = null
|
|
719
|
+
if (args.logo_image) {
|
|
720
|
+
try {
|
|
721
|
+
const src = await resolveConversationImage(ctx, exec, args.logo_image)
|
|
722
|
+
if (src && src.bytes) {
|
|
723
|
+
const b64 = Buffer.isBuffer(src.bytes) ? src.bytes.toString('base64') : Buffer.from(src.bytes).toString('base64')
|
|
724
|
+
logoDataUri = `data:${src.mediaType || 'image/png'};base64,${b64}`
|
|
725
|
+
}
|
|
726
|
+
} catch (_) {}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const exportedFiles = []
|
|
730
|
+
|
|
731
|
+
// 1. Favicon SVG
|
|
732
|
+
const faviconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
733
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
734
|
+
<rect width="64" height="64" rx="16" fill="${themeColor}"/>
|
|
735
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="8" y="8" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>` : `<text x="32" y="44" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="34" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
736
|
+
</svg>`
|
|
737
|
+
const faviconPath = path.join(targetDir, 'favicon.svg')
|
|
738
|
+
await writeFile(faviconPath, faviconSvg, 'utf8')
|
|
739
|
+
exportedFiles.push(faviconPath)
|
|
740
|
+
|
|
741
|
+
// 2. Icon 192 & 512
|
|
742
|
+
for (const size of [192, 512]) {
|
|
743
|
+
const rx = Math.round(size * 0.22)
|
|
744
|
+
const fontSize = Math.round(size * 0.52)
|
|
745
|
+
const yOffset = Math.round(size * 0.68)
|
|
746
|
+
const iconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
747
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
748
|
+
<defs>
|
|
749
|
+
<linearGradient id="brandGrad-${size}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
750
|
+
<stop offset="0%" stop-color="${themeColor}"/>
|
|
751
|
+
<stop offset="100%" stop-color="#1e1b4b"/>
|
|
752
|
+
</linearGradient>
|
|
753
|
+
</defs>
|
|
754
|
+
<rect width="${size}" height="${size}" rx="${rx}" fill="url(#brandGrad-${size})"/>
|
|
755
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="${size * 0.15}" y="${size * 0.15}" width="${size * 0.7}" height="${size * 0.7}" preserveAspectRatio="xMidYMid meet"/>` : `<text x="${size / 2}" y="${yOffset}" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="${fontSize}" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
756
|
+
</svg>`
|
|
757
|
+
const iconPath = path.join(targetDir, `icon-${size}.svg`)
|
|
758
|
+
await writeFile(iconPath, iconSvg, 'utf8')
|
|
759
|
+
exportedFiles.push(iconPath)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 3. Web App Manifest
|
|
763
|
+
const manifest = {
|
|
764
|
+
name: projectName,
|
|
765
|
+
short_name: projectName.slice(0, 12),
|
|
766
|
+
description: tagline,
|
|
767
|
+
start_url: '/',
|
|
768
|
+
display: 'standalone',
|
|
769
|
+
background_color: '#0b0c0e',
|
|
770
|
+
theme_color: themeColor,
|
|
771
|
+
icons: [
|
|
772
|
+
{ src: 'favicon.svg', sizes: '64x64', type: 'image/svg+xml', purpose: 'any' },
|
|
773
|
+
{ src: 'icon-192.svg', sizes: '192x192', type: 'image/svg+xml', purpose: 'any maskable' },
|
|
774
|
+
{ src: 'icon-512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
|
|
775
|
+
],
|
|
776
|
+
}
|
|
777
|
+
const manifestPath = path.join(targetDir, 'manifest.webmanifest')
|
|
778
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
779
|
+
exportedFiles.push(manifestPath)
|
|
780
|
+
|
|
781
|
+
// 4. OpenGraph Social Card (1200x630)
|
|
782
|
+
const escapedProject = projectName.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
783
|
+
const escapedTagline = tagline.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
784
|
+
const ogCardSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
785
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
786
|
+
<defs>
|
|
787
|
+
<radialGradient id="bgGlow" cx="75%" cy="25%" r="70%">
|
|
788
|
+
<stop offset="0%" stop-color="${themeColor}" stop-opacity="0.35"/>
|
|
789
|
+
<stop offset="60%" stop-color="#0b0c0e" stop-opacity="0.95"/>
|
|
790
|
+
<stop offset="100%" stop-color="#050506" stop-opacity="1"/>
|
|
791
|
+
</radialGradient>
|
|
792
|
+
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
793
|
+
<feDropShadow dx="0" dy="16" stdDeviation="24" flood-color="#000000" flood-opacity="0.6"/>
|
|
794
|
+
</filter>
|
|
795
|
+
</defs>
|
|
796
|
+
<rect width="1200" height="630" fill="url(#bgGlow)"/>
|
|
797
|
+
<rect x="80" y="80" width="1040" height="470" rx="24" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" filter="url(#shadow)"/>
|
|
798
|
+
|
|
799
|
+
<rect x="130" y="140" width="110" height="32" rx="16" fill="${themeColor}" fill-opacity="0.2" stroke="${themeColor}" stroke-opacity="0.4"/>
|
|
800
|
+
<text x="185" y="161" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="12" font-weight="700" text-anchor="middle" letter-spacing="1">DSH STUDIO</text>
|
|
801
|
+
|
|
802
|
+
<text x="130" y="250" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="56" font-weight="800">${escapedProject}</text>
|
|
803
|
+
<text x="130" y="310" fill="#94a3b8" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="22" font-weight="400">${escapedTagline}</text>
|
|
804
|
+
|
|
805
|
+
<g transform="translate(860, 200)">
|
|
806
|
+
<rect width="180" height="180" rx="36" fill="${themeColor}" fill-opacity="0.15" stroke="${themeColor}" stroke-width="2"/>
|
|
807
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="20" y="20" width="140" height="140" preserveAspectRatio="xMidYMid meet"/>` : `<text x="90" y="125" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="96" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
808
|
+
</g>
|
|
809
|
+
|
|
810
|
+
<text x="130" y="480" fill="#64748b" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="14" font-weight="500">Auto-generated by @goodandready/dsh-image-gen</text>
|
|
811
|
+
</svg>`
|
|
812
|
+
const ogCardPath = path.join(targetDir, 'og-card.svg')
|
|
813
|
+
await writeFile(ogCardPath, ogCardSvg, 'utf8')
|
|
814
|
+
exportedFiles.push(ogCardPath)
|
|
815
|
+
|
|
816
|
+
// 5. Catalog Index JSON
|
|
817
|
+
const catalog = {
|
|
818
|
+
project: projectName,
|
|
819
|
+
tagline,
|
|
820
|
+
themeColor,
|
|
821
|
+
generatedAt: new Date().toISOString(),
|
|
822
|
+
generator: '@goodandready/dsh-image-gen',
|
|
823
|
+
assets: exportedFiles.map((f) => path.basename(f)),
|
|
824
|
+
}
|
|
825
|
+
const catalogPath = path.join(targetDir, 'catalog.json')
|
|
826
|
+
await writeFile(catalogPath, JSON.stringify(catalog, null, 2), 'utf8')
|
|
827
|
+
exportedFiles.push(catalogPath)
|
|
828
|
+
|
|
829
|
+
const summary = `### Exported Brand Asset Pack (${projectName})\n`
|
|
830
|
+
+ `- **Destination**: \`${targetDir}\`\n`
|
|
831
|
+
+ `- **Files Generated**: ${exportedFiles.length} brand files\n`
|
|
832
|
+
+ ` - \`favicon.svg\`, \`icon-192.svg\`, \`icon-512.svg\`\n`
|
|
833
|
+
+ ` - \`manifest.webmanifest\` (PWA ready)\n`
|
|
834
|
+
+ ` - \`og-card.svg\` (1200×630 OpenGraph preview card)\n`
|
|
835
|
+
+ ` - \`catalog.json\` (Asset registry index)\n\n`
|
|
836
|
+
+ ``
|
|
837
|
+
|
|
838
|
+
return toLosslessJson({
|
|
839
|
+
summary,
|
|
840
|
+
manifestPath,
|
|
841
|
+
ogCardPath,
|
|
842
|
+
catalogPath,
|
|
843
|
+
exportedFiles,
|
|
844
|
+
})
|
|
845
|
+
} catch (err) {
|
|
846
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
}),
|
|
850
|
+
)
|
|
851
|
+
}, 'dsh-image-gen: tool export_asset_pack')
|
|
852
|
+
|
|
853
|
+
|
|
496
854
|
}
|
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.21",
|
|
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",
|